Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/roadmap-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ Este documento contém o registro de todas as sprints concluídas para fins de a

---

## ✅ Sprint 13 - RabbitMQ Excellence & i18n Tests (Concluída em 28 Abr 2026)

**Objetivo**: Consolidação total da infraestrutura de mensageria, i18n frontend e UI/UX Admin.

### Entregas:
- ✅ **RabbitMQ Excellence**: Implementação real do `RabbitMqInfrastructureManager` com métodos assíncronos (`CreateQueueAsync`, `CreateExchangeAsync`, `BindQueueToExchangeAsync`), eliminando stubs.
- ✅ **Deadlock Fix**: Remoção de deadlocks em `CreateQueueAsync`, `CreateExchangeAsync`, `BindQueueToExchangeAsync` - agora `GetChannelAsync` gerencia lock internamente.
- ✅ **Safe Dispose**: `DisposeAsync` agora adquire lock antes de dispose e usa flag `_disposed` para prevenir operações pós-descarte.
- ✅ **Fail-Fast DI**: `MessagingExtensions` agora lança `InvalidOperationException` quando `IRabbitMqInfrastructureManager` não está registrado (em vez de retornar silenciosamente).
- ✅ **i18n Frontend**: Implementação de `useTranslation` no dashboard Admin, tradução de labels via i18n (`t()`), uso de `useMemo` para otimização.
- ✅ **i18n Test Mocks**: Implementação de mocks de i18next para testes em Admin, Provider e Customer apps com suporte a `defaultValue` (incluindo strings vazias).
- ✅ **UI/UX Admin Portal**: Aplicação de cores da marca (laranja #D96704, brand #E0702B, cream #FDFBF7) no CSS, variáveis CSS para cores secundárias, `data-testid` únicos em CardTitle.
- ✅ **Testes Unitários**: Cobertura superior a 90%, testes de `MessagingExtensions` e `RabbitMqInfrastructureManager` atualizados e passando.

---

## ✅ Sprint 12 - Bookings & Messaging Excellence (Concluída em 26 Abr 2026)

**Objetivo**: Implementar o sistema de agendamentos e consolidar a infraestrutura de mensageria com Rebus.
Expand Down
14 changes: 6 additions & 8 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi.

## 📊 Status Atual (Abril 2026)

**Sprint Atual**: 13 (Infraestrutura & Excelência UX)
**Sprint Atual**: 14 (Próxima)

**Status**: 🛠️ Em Execução (Início em 27 Abr 2026)
**Status**: ✅ Sprint 13 Concluída (28 Abr 2026)

**Meta MVP**: 12 a 16 de maio de 2026

Expand All @@ -18,10 +18,7 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi.

## 🔮 Roadmaps Futuros (MVP Launch & Além)

### Fase 3: Escala e Provedores Reais (Em Execução)
* **[x] RabbitMQ Excellence**: Consolidação total da infraestrutura de mensageria, removendo stubs e dependências diretas de driver.
* **[ ] i18n Apps Provider/Admin**: Implementação de localização frontend para os apps de Prestador e Administrador.
* **[ ] UI/UX Admin Portal**: Padronização visual com cores da marca e melhorias de usabilidade no portal administrativo.
### Fase 3: Escala e Provedores Reais
* **Provedores de Comunicação (Próximo)**: Substituir Stubs por SendGrid (E-mail), Twilio (SMS) e Firebase (Push).
* **Verificação Automatizada (Próximo)**: OCR via Azure AI Vision e integração com APIs de antecedentes criminais.

Expand All @@ -38,8 +35,9 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi.

## ✅ Concluído Recentemente

* **Sprint 11**: Monetização completa (Checkout, Webhooks, Billing Portal, Renovação Automática), Localização i18n Frontend, Skeleton Loaders e cobertura de testes abrangente. (Abril 2026)
* **Sprint 10**: Módulo de Ratings, Moderação de Conteúdo, Login Social Instagram (#141), Alinhamento de Realms Keycloak, Infra CI/CD (OpenAPI gating) e Documentação (coleções Bruno). (Abril 2026)
* **Sprint 13**: RabbitMQ Excellence (infraestrutura real com RabbitMqInfrastructureManager, deadlocks corrigidos, dispose seguro), i18n mocks para testes (Provider/Admin/Customer), fail-fast em DI de Messaging.
* **Sprint 12**: Bookings Module completo, Command Handlers (Reject/Complete), queries de listagem, automação com Domain Events, integração frontend de agenda.
* **Sprint 11**: Monetização completa (Checkout, Webhooks, Billing Portal, Renovação Automática), Localização i18n Frontend, Skeleton Loaders e cobertura de testes abrangente.

---

Expand Down
17 changes: 8 additions & 9 deletions docs/technical-debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@ Este documento rastreia **débitos técnicos e seu histórico de otimização**.

- [ ] Perfilagem de memória em produção

### 🎨 Melhorias de UI/UX
---

**Severidade**: BAIXA
**Sprint**: Backlog
## 📋 Histórico

- [ ] Aplicar cores da marca (azul, creme, laranja) em todo o Portal Admin (React)
- [ ] Atualizar o tema da biblioteca de componentes React
- [ ] Padronizar a estilização dos componentes
### 🎨 UI/UX Admin Portal - Cores da Marca

**Origem**: Sprint 7.19
**Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: BAIXA
Aplicação de cores da marca (laranja #D96704, brand #E0702B, cream #FDFBF7) no CSS do Admin, variáveis CSS para cores secundárias, ring atualizado para cor da marca, e correção de `data-testid` duplicados em CardTitle.

---
### 🌍 i18n Frontend Apps (Admin/Provider/Customer)

## 📋 Histórico
**Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: MÉDIA
Implementação de `useTranslation` no dashboard Admin, tradução de labels via i18n, e mocks de i18next para testes em todos os apps (Admin/Provider/Customer) com suporte a `defaultValue` (incluindo strings vazias).

### 🚀 Infraestrutura & Messaging (RabbitMQ Excellence)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ private static string GetClientIpAddress(HttpContext context)
var xForwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(xForwardedFor))
{
return xForwardedFor.Split(',')[0].Trim();
var span = xForwardedFor.AsSpan();
var commaIndex = span.IndexOf(',');
return (commaIndex >= 0 ? span[..commaIndex] : span).Trim().ToString();
}

var xRealIp = context.Request.Headers["X-Real-IP"].FirstOrDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,38 @@ public async Task<Result<UserDto>> HandleAsync(RegisterCustomerCommand command,
try
{
emailAsValueObject = new Email(command.Email);

var fullLocalPart = emailAsValueObject.Value.Split('@')[0];
var noTagLocalPart = fullLocalPart.Split('+')[0];
var sanitizedLocalPart = SanitizationRegex().Replace(noTagLocalPart, "");


var emailSpan = emailAsValueObject.Value.AsSpan();
var atIdx = emailSpan.IndexOf('@');
var localSpan = atIdx >= 0 ? emailSpan[..atIdx] : emailSpan;
var plusIdx = localSpan.IndexOf('+');
var noTagSpan = plusIdx >= 0 ? localSpan[..plusIdx] : localSpan;
var sanitizedLocalPart = SanitizationRegex().Replace(noTagSpan.ToString(), "");

if (string.IsNullOrWhiteSpace(sanitizedLocalPart) || sanitizedLocalPart.Length < 3)
{
sanitizedLocalPart = $"usr{Guid.NewGuid().ToString("N").Substring(0, 5)}";
sanitizedLocalPart = $"usr{Guid.NewGuid().ToString("N").AsSpan(0, 5).ToString()}";
}

// UsernameMaxLength é 30 em ValidationConstants; deduz 1 para '_' e 6 para GUID => localPartMax = UsernameMaxLength - 7
int maxLocalPartLength = ValidationConstants.UserLimits.UsernameMaxLength - 7;
if (sanitizedLocalPart.Length > maxLocalPartLength)
{
sanitizedLocalPart = sanitizedLocalPart.Substring(0, maxLocalPartLength);
sanitizedLocalPart = sanitizedLocalPart[..maxLocalPartLength];
}
var slug = $"{sanitizedLocalPart}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";

var slug = $"{sanitizedLocalPart}_{Guid.NewGuid().ToString("N").AsSpan(0, 6).ToString()}";
validUsername = new Username(slug);
}
catch (ArgumentException ex)
{
return Result<UserDto>.Failure(Error.BadRequest(ex.Message));
}

var emailParts = command.Email.Split('@');
var maskedEmail = emailParts.Length == 2
? $"{new string('*', Math.Min(3, emailParts[0].Length))}@{emailParts[1]}"
var rawEmailSpan = command.Email.AsSpan();
var atSeparator = rawEmailSpan.IndexOf('@');
var maskedEmail = atSeparator >= 0
? $"{new string('*', Math.Min(3, atSeparator))}@{rawEmailSpan[(atSeparator + 1)..].ToString()}"
: "***@***";

// Valida unicidade primeiro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,19 @@ private static bool IsAdminPath(string path)
if (string.IsNullOrEmpty(path))
return false;

var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
return segments.Any(segment =>
string.Equals(segment, "admin", StringComparison.OrdinalIgnoreCase));
var span = path.AsSpan();
while (span.Length > 0)
{
var slashIndex = span.IndexOf('/');
var segment = slashIndex >= 0 ? span[..slashIndex] : span;
if (segment.Equals("admin".AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;
if (slashIndex < 0)
break;
span = span[(slashIndex + 1)..];
}

return false;
}

/// <summary>
Expand Down
16 changes: 8 additions & 8 deletions src/Shared/Caching/CacheMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ private static string NormalizeCacheKey(string key)
if (string.IsNullOrEmpty(key))
return "empty";

var parts = key.Split(':');
if (parts.Length >= 2)
var colonIndex = key.AsSpan().IndexOf(':');
if (colonIndex >= 0)
{
var type = parts[0];
if (type.Equals("user", StringComparison.OrdinalIgnoreCase))
var type = key.AsSpan()[..colonIndex];
if (type.Equals("user".AsSpan(), StringComparison.OrdinalIgnoreCase))
return "user:{id}";
if (type.Equals("provider", StringComparison.OrdinalIgnoreCase))
if (type.Equals("provider".AsSpan(), StringComparison.OrdinalIgnoreCase))
return "provider:{id}";
if (type.Equals("permission", StringComparison.OrdinalIgnoreCase))
if (type.Equals("permission".AsSpan(), StringComparison.OrdinalIgnoreCase))
return "permission:{id}";
if (type.Equals("role", StringComparison.OrdinalIgnoreCase))
if (type.Equals("role".AsSpan(), StringComparison.OrdinalIgnoreCase))
return "role:{id}";
return $"{type}:{{id}}";
return $"{type.ToString()}:{{id}}";
}

if (key.Length > 20)
Expand Down
24 changes: 20 additions & 4 deletions src/Shared/Monitoring/BusinessMetricsMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,36 @@ private void LogBusinessEvents(HttpContext context, TimeSpan elapsed)
}

// Conclusão de ajuda (aceita rotas versionadas como /api/v1/help-requests/{id}/complete)
if (((path.StartsWith("/api/help-requests/") && path.EndsWith("/complete")) ||
(path.StartsWith("/api/v") && path.Contains("/help-requests/") && path.EndsWith("/complete"))) &&
method == "POST" && statusCode is >= 200 and < 300)
if (IsHelpRequestCompletePath(path) && method == "POST" && statusCode is >= 200 and < 300)
{
businessMetrics.RecordHelpRequestCompleted("general", elapsed);
logger.LogInformation("Help request completed in {ElapsedMs}ms", elapsed.TotalMilliseconds);
}
}
}

/// <summary>
/// Verifica se o path corresponde a uma conclusão de help-request (rotas simples ou versionadas).
/// Usa ReadOnlySpan para evitar alocações em um hot path executado a cada requisição POST.
/// </summary>
private static bool IsHelpRequestCompletePath(string path)
{
var span = path.AsSpan();
ReadOnlySpan<char> completeSuffix = "/complete".AsSpan();
if (!span.EndsWith(completeSuffix, StringComparison.Ordinal))
return false;

ReadOnlySpan<char> directPrefix = "/api/help-requests/".AsSpan();
ReadOnlySpan<char> segment = "/help-requests/".AsSpan();
return span.StartsWith(directPrefix, StringComparison.Ordinal)
|| (span.StartsWith("/api/v".AsSpan(), StringComparison.Ordinal)
&& span.Contains(segment, StringComparison.Ordinal));
}

private static string GetEndpointName(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint != null)

{
return endpoint.DisplayName ?? context.Request.Path.Value ?? "unknown";
}
Expand Down