diff --git a/.env.example b/.env.example index 7d7b76d7..d886060d 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,8 @@ STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_URL_BASE=http://localhost:3000 # STRIPE_TAXA_PLATAFORMA=5 +# Stripe__ExpectLivemode=true # OBRIGATÓRIO em produção — boot falha se sk_live_ e este valor não for true. +# # Omitir em Dev/Homolog (default=false → test-mode permitido). # Seed admin local SEED_ADMIN_EMAIL=admin@forzion.tech @@ -36,6 +38,7 @@ SEED_ADMIN_PASSWORD=SuaSenhaAdmin123! # Chave do endpoint interno /internal/processar-renovacoes (cron de cobrança mensal) # Gerar: openssl rand -hex 32. Mesmo valor deve estar no GitHub secret INTERNAL_API_KEY. # Vazio = endpoint rejeita todas as requisições (401). +# Production: AUSENTE => app falha no startup (fail-closed). Homolog/Dev: ausente => sobe. # INTERNAL_API_KEY= # Pseudonimização do destinatário nos delivery logs (PRIV-02 / LGPD). diff --git a/.github/workflows/billing-prenotification.yml b/.github/workflows/billing-prenotification.yml index 6935cc77..36232512 100644 --- a/.github/workflows/billing-prenotification.yml +++ b/.github/workflows/billing-prenotification.yml @@ -44,6 +44,13 @@ jobs: "http://localhost:8080/internal/$ROTA" done + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-prenotification" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-prenotification/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 diff --git a/.github/workflows/billing-reconciliation.yml b/.github/workflows/billing-reconciliation.yml index 9ef0ccc3..f5f7133c 100644 --- a/.github/workflows/billing-reconciliation.yml +++ b/.github/workflows/billing-reconciliation.yml @@ -52,6 +52,13 @@ jobs: -d "$BODY" \ http://localhost:8080/internal/reconciliar-pagamentos + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-reconciliation" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-reconciliation/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 @@ -67,10 +74,12 @@ jobs: '', 'Sem reconciliação = webhooks Stripe perdidos podem ficar não detectados (Pagamento.Status diverge do estado real).', '', + '**HTTP 503 = batch truncado** (backlog > teto por run): o cursor avançou só até o último evento processado, nenhum evento foi perdido. O próximo run agendado continua de onde parou. Para drenar agora, re-disparar manualmente via `workflow_dispatch` (deixar `desde_utc` vazio = retoma do cursor; ou informar `desde_utc` ISO-8601 p/ backfill de janela específica).', + '', 'Investigar (ordem dos layers):', '1. Tailnet: runner conectou? ACL `tag:ci -> VM:22` (SSH) liberada?', '2. SSH: `VM_TAILNET_IP` bate com a VM? `HOMOLOG_SSH_KEY` válida?', - '3. `docker compose exec backend` rodou? `/internal/reconciliar-pagamentos` respondeu (401=key, 5xx=backend)?', + '3. `docker compose exec backend` rodou? `/internal/reconciliar-pagamentos` respondeu (401=key, 503=batch truncado, 5xx=backend)?', '4. Stripe API rate limit estourou (`stripe_request_quota_exceeded`)?', '5. Backend logs (`docker compose logs backend --tail 200`)', ].join('\n'); diff --git a/.github/workflows/billing-renewal-treinador.yml b/.github/workflows/billing-renewal-treinador.yml index 89052fd8..11ee4f16 100644 --- a/.github/workflows/billing-renewal-treinador.yml +++ b/.github/workflows/billing-renewal-treinador.yml @@ -42,6 +42,13 @@ jobs: -H "X-Internal-Key: $KEY" -H "Content-Type: application/json" \ http://localhost:8080/internal/processar-renovacoes-treinador + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-renewal-treinador" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-renewal-treinador/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 diff --git a/.github/workflows/billing-renewal.yml b/.github/workflows/billing-renewal.yml index 2da749b9..1a55c6f8 100644 --- a/.github/workflows/billing-renewal.yml +++ b/.github/workflows/billing-renewal.yml @@ -42,6 +42,13 @@ jobs: -H "X-Internal-Key: $KEY" -H "Content-Type: application/json" \ http://localhost:8080/internal/processar-renovacoes + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-renewal" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/billing-renewal/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 diff --git a/.github/workflows/db-backup.yml b/.github/workflows/db-backup.yml index cbecc1a7..cb3d5abd 100644 --- a/.github/workflows/db-backup.yml +++ b/.github/workflows/db-backup.yml @@ -95,6 +95,13 @@ jobs: --only-show-errors echo "backup enviado: s3://${R2_BUCKET}/${FILE}.age (${SIZE} bytes em claro)" + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/db-backup" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/db-backup/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 diff --git a/.github/workflows/gerar-nfse-comissao.yml b/.github/workflows/gerar-nfse-comissao.yml index 1a8fe35a..d6469138 100644 --- a/.github/workflows/gerar-nfse-comissao.yml +++ b/.github/workflows/gerar-nfse-comissao.yml @@ -42,6 +42,13 @@ jobs: -H "X-Internal-Key: $KEY" -H "Content-Type: application/json" \ http://localhost:8080/internal/gerar-nfse-comissao + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/gerar-nfse-comissao" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/gerar-nfse-comissao/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v7 diff --git a/.github/workflows/lgpd-purge.yml b/.github/workflows/lgpd-purge.yml index df8fa29a..52cf621a 100644 --- a/.github/workflows/lgpd-purge.yml +++ b/.github/workflows/lgpd-purge.yml @@ -71,6 +71,13 @@ jobs: exit 1 fi + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/lgpd-purge" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/lgpd-purge/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v9 diff --git a/.github/workflows/reconciliar-nfse.yml b/.github/workflows/reconciliar-nfse.yml index 80da46f5..cb3c461c 100644 --- a/.github/workflows/reconciliar-nfse.yml +++ b/.github/workflows/reconciliar-nfse.yml @@ -42,6 +42,13 @@ jobs: -H "X-Internal-Key: $KEY" -H "Content-Type: application/json" \ http://localhost:8080/internal/reconciliar-nfse + - name: Healthchecks ping (success) + if: success() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/reconciliar-nfse" || true + - name: Healthchecks ping (failure) + if: failure() + run: curl -fsS -m 10 --retry 3 "https://hc-ping.com/${{ secrets.HC_PING_KEY }}/reconciliar-nfse/fail" || true + - name: Notificar falha via issue if: failure() uses: actions/github-script@v7 diff --git a/.github/workflows/zap.yml b/.github/workflows/zap.yml index 264724d2..18e4125c 100644 --- a/.github/workflows/zap.yml +++ b/.github/workflows/zap.yml @@ -48,7 +48,7 @@ jobs: - name: ZAP Baseline Scan (passivo) if: ${{ github.event.inputs.mode != 'full' }} - uses: zaproxy/action-baseline@v0.14.0 + uses: zaproxy/action-baseline@v0.15.0 with: target: ${{ steps.t.outputs.target }} cmd_options: "-a" diff --git a/docs/api/openapi.v1.json b/docs/api/openapi.v1.json index 4e7d846f..df6e685d 100644 --- a/docs/api/openapi.v1.json +++ b/docs/api/openapi.v1.json @@ -104,6 +104,16 @@ } } }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReconciliarPagamentosStripeResponse" + } + } + } + }, "401": { "description": "Unauthorized", "content": { @@ -3551,6 +3561,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "404": { "description": "Not Found", "content": { @@ -10476,7 +10496,9 @@ "replayed", "jaConsistentes", "erros", - "desdeUtc" + "desdeUtc", + "truncado", + "onboardingConfirmados" ], "type": "object", "properties": { @@ -10515,6 +10537,17 @@ "desdeUtc": { "type": "string", "format": "date-time" + }, + "truncado": { + "type": "boolean" + }, + "onboardingConfirmados": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" } } }, diff --git a/forzion.tech.Api/Endpoints/Pagamentos/PagamentosEndpoints.cs b/forzion.tech.Api/Endpoints/Pagamentos/PagamentosEndpoints.cs index 835734e0..0d14d2b8 100644 --- a/forzion.tech.Api/Endpoints/Pagamentos/PagamentosEndpoints.cs +++ b/forzion.tech.Api/Endpoints/Pagamentos/PagamentosEndpoints.cs @@ -315,6 +315,11 @@ public static IEndpointRouteBuilder MapPagamentosEndpoints(this IEndpointRouteBu var result = await handler.HandleAsync(command, cancellationToken).ConfigureAwait(false); if (result.IsFailure) return result.ToProblemResult(); + + // 503 p/ o curl -fsS do cron falhar (if:failure() abre issue) quando há backlog restante. + if (result.Value.Truncado) + return Results.Json(result.Value, statusCode: StatusCodes.Status503ServiceUnavailable); + return Results.Ok(result.Value); }) .WithTags("Internal") @@ -322,6 +327,7 @@ public static IEndpointRouteBuilder MapPagamentosEndpoints(this IEndpointRouteBu .AllowAnonymous() .RequireRateLimiting("internal") .Produces() + .Produces(StatusCodes.Status503ServiceUnavailable) .ProducesProblem(StatusCodes.Status401Unauthorized); return endpoints; diff --git a/forzion.tech.Api/Endpoints/Treinador/TreinadorEndpoints.cs b/forzion.tech.Api/Endpoints/Treinador/TreinadorEndpoints.cs index c58b3119..c093a023 100644 --- a/forzion.tech.Api/Endpoints/Treinador/TreinadorEndpoints.cs +++ b/forzion.tech.Api/Endpoints/Treinador/TreinadorEndpoints.cs @@ -170,6 +170,7 @@ public static IEndpointRouteBuilder MapTreinadorEndpoints(this IEndpointRouteBui if (result.IsFailure) return result.ToProblemResult(); return Results.Ok(result.Value); }) + .AddEndpointFilter() .WithSummary("Aprova o vínculo de um aluno ao treinador") .Produces() .ProducesProblem(StatusCodes.Status403Forbidden) @@ -206,8 +207,10 @@ public static IEndpointRouteBuilder MapTreinadorEndpoints(this IEndpointRouteBui if (result.IsFailure) return result.ToProblemResult(); return Results.Ok(result.Value); }) + .AddEndpointFilter() .WithSummary("Reativa um aluno inativo criando um novo vínculo aprovado") .Produces() + .ProducesProblem(StatusCodes.Status403Forbidden) .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status422UnprocessableEntity); @@ -344,6 +347,7 @@ public static IEndpointRouteBuilder MapTreinadorEndpoints(this IEndpointRouteBui if (result.IsFailure) return result.ToProblemResult(); return Results.NoContent(); }) + .AddEndpointFilter() .WithSummary("Vincula uma ficha de treino a um aluno") .Produces(StatusCodes.Status204NoContent) .ProducesProblem(StatusCodes.Status403Forbidden) diff --git a/forzion.tech.Api/Endpoints/Treinos/TreinoEndpoints.cs b/forzion.tech.Api/Endpoints/Treinos/TreinoEndpoints.cs index c88294f3..9ce4df6b 100644 --- a/forzion.tech.Api/Endpoints/Treinos/TreinoEndpoints.cs +++ b/forzion.tech.Api/Endpoints/Treinos/TreinoEndpoints.cs @@ -44,6 +44,7 @@ public static void MapTreinoEndpoints(this IEndpointRouteBuilder app) return Results.Created($"/treinos/{result.Value.TreinoId}", result.Value); }) .RequireAuthorization() + .AddEndpointFilter() .WithSummary("Cria um novo treino para um aluno") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status401Unauthorized) @@ -134,6 +135,7 @@ public static void MapTreinoEndpoints(this IEndpointRouteBuilder app) return Results.NoContent(); }) .RequireAuthorization() + .AddEndpointFilter() .WithSummary("Vincula uma ficha de treino a um aluno") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) @@ -239,6 +241,7 @@ public static void MapTreinoEndpoints(this IEndpointRouteBuilder app) return Results.Created($"/treinos/{result.Value.TreinoId}", result.Value); }) .RequireAuthorization() + .AddEndpointFilter() .WithSummary("Duplica um treino (gera uma cópia)") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status401Unauthorized) diff --git a/forzion.tech.Api/Filters/RequireAssinaturaTreinadorAtivaFilter.cs b/forzion.tech.Api/Filters/RequireAssinaturaTreinadorAtivaFilter.cs index 5f2ea27d..7bad725b 100644 --- a/forzion.tech.Api/Filters/RequireAssinaturaTreinadorAtivaFilter.cs +++ b/forzion.tech.Api/Filters/RequireAssinaturaTreinadorAtivaFilter.cs @@ -1,4 +1,5 @@ using forzion.tech.Application.Interfaces; +using forzion.tech.Application.Interfaces.Repositories; using forzion.tech.Domain.Enums; namespace forzion.tech.Api.Filters; @@ -7,15 +8,19 @@ public sealed class RequireAssinaturaTreinadorAtivaFilter : RequireAssinaturaAti { protected override string CodigoErro => "ASSINATURA_TREINADOR_INADIMPLENTE"; - protected override Task EstaInadimplenteAsync( + protected override async Task EstaInadimplenteAsync( IServiceProvider services, IUserContext userContext, CancellationToken ct) { if (userContext.TipoConta != TipoConta.Treinador) - return Task.FromResult(false); + return false; - // Quando billing do treinador for implementado, consultar IAssinaturaTreinadorRepository aqui. - return Task.FromResult(false); + var assinaturaRepository = services.GetRequiredService(); + var assinaturaAtual = await assinaturaRepository + .ObterAtualPorTreinadorAsync(userContext.PerfilId, ct) + .ConfigureAwait(false); + + return assinaturaAtual?.Status == AssinaturaTreinadorStatus.Inadimplente; } } diff --git a/forzion.tech.Api/forzion.tech.Api.csproj b/forzion.tech.Api/forzion.tech.Api.csproj index 57b5cb0f..47b5a5f7 100644 --- a/forzion.tech.Api/forzion.tech.Api.csproj +++ b/forzion.tech.Api/forzion.tech.Api.csproj @@ -29,7 +29,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/forzion.tech.Application/Interfaces/IStripeService.cs b/forzion.tech.Application/Interfaces/IStripeService.cs index 6382bb76..f0d9c288 100644 --- a/forzion.tech.Application/Interfaces/IStripeService.cs +++ b/forzion.tech.Application/Interfaces/IStripeService.cs @@ -33,9 +33,10 @@ public interface IStripeService /// Lista eventos Stripe criados a partir de , filtrando /// pelos tipos relevantes para reconciliação de pagamentos e onboarding Connect. /// Resultado ordenado por ASC (mais antigo primeiro) - /// e capado para um teto interno de segurança (evita varreduras patológicas). + /// e capado por um teto de batch (memória bounded); + /// sinaliza que o cap foi atingido e há backlog restante para o próximo run. /// - Task> ListarEventosDesdeAsync(DateTime desdeUtc, CancellationToken cancellationToken = default); + Task ListarEventosDesdeAsync(DateTime desdeUtc, CancellationToken cancellationToken = default); } public enum CancelarPaymentIntentResultado @@ -77,3 +78,12 @@ public sealed record StripeEventSummary( string Type, string PayloadRaw, DateTime Created); + +/// +/// Lote de eventos retornado por Events.List. Truncado=true quando a varredura +/// parou no teto de batch antes de drenar a janela — o reconciliador avança o cursor só até o +/// último evento processado e sinaliza para o cron re-disparar a partir do cursor. +/// +public sealed record StripeEventListResult( + IReadOnlyList Eventos, + bool Truncado); diff --git a/forzion.tech.Application/Interfaces/Repositories/IContaRecebimentoRepository.cs b/forzion.tech.Application/Interfaces/Repositories/IContaRecebimentoRepository.cs index 142357e2..ba68d1a8 100644 --- a/forzion.tech.Application/Interfaces/Repositories/IContaRecebimentoRepository.cs +++ b/forzion.tech.Application/Interfaces/Repositories/IContaRecebimentoRepository.cs @@ -6,5 +6,6 @@ public interface IContaRecebimentoRepository { Task ObterPorTreinadorIdAsync(Guid treinadorId, CancellationToken cancellationToken = default); Task ObterPorStripeAccountIdAsync(string stripeAccountId, CancellationToken cancellationToken = default); + Task> ListarConfiguradasPendentesOnboardingAsync(int max, CancellationToken cancellationToken = default); Task AdicionarAsync(ContaRecebimento conta, CancellationToken cancellationToken = default); } diff --git a/forzion.tech.Application/Interfaces/Repositories/IReconciliacaoStripeEstadoRepository.cs b/forzion.tech.Application/Interfaces/Repositories/IReconciliacaoStripeEstadoRepository.cs new file mode 100644 index 00000000..2c1942d6 --- /dev/null +++ b/forzion.tech.Application/Interfaces/Repositories/IReconciliacaoStripeEstadoRepository.cs @@ -0,0 +1,9 @@ +using forzion.tech.Domain.Entities; + +namespace forzion.tech.Application.Interfaces.Repositories; + +public interface IReconciliacaoStripeEstadoRepository +{ + Task ObterAsync(CancellationToken cancellationToken = default); + Task SalvarAsync(ReconciliacaoStripeEstado estado, CancellationToken cancellationToken = default); +} diff --git a/forzion.tech.Application/Settings/InternalSettings.cs b/forzion.tech.Application/Settings/InternalSettings.cs new file mode 100644 index 00000000..dcac36fa --- /dev/null +++ b/forzion.tech.Application/Settings/InternalSettings.cs @@ -0,0 +1,6 @@ +namespace forzion.tech.Application.Settings; + +public class InternalSettings +{ + public string ApiKey { get; set; } = string.Empty; +} diff --git a/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/ProcessarWebhookStripeHandler.cs b/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/ProcessarWebhookStripeHandler.cs index 5f7ee1bb..c2becb9c 100644 --- a/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/ProcessarWebhookStripeHandler.cs +++ b/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/ProcessarWebhookStripeHandler.cs @@ -321,7 +321,7 @@ private async Task ProcessarChargeReembolsadoAsync(str { var pagamentoTreinador = await pagamentoTreinadorRepository.ObterPorStripePaymentIntentIdAsync(paymentIntentId, ct).ConfigureAwait(false); if (pagamentoTreinador is not null) - return await ProcessarEstornoTreinadorAsync(pagamentoTreinador, ct).ConfigureAwait(false); + return await ProcessarEstornoTreinadorAsync(pagamentoTreinador, amountRefundedCents, ct).ConfigureAwait(false); logger.LogWarning("charge.refunded para PaymentIntent {PaymentIntentId} não encontrado.", paymentIntentId); return ProcessarEventoResultado.JaConsistente; @@ -341,6 +341,9 @@ private async Task ProcessarChargeReembolsadoAsync(str return ProcessarEventoResultado.JaConsistente; } + if (!amountRefundedCents.HasValue) + throw new InvalidOperationException($"charge.refunded para PaymentIntent {paymentIntentId} sem amount_refunded. Retry necessário."); + // só transiciona para Estornado em refund total — parcial deixa em Pago. var valorPagamentoCents = (long)Math.Round(pagamento.Valor * 100m, MidpointRounding.AwayFromZero); if (amountRefundedCents.HasValue && amountRefundedCents.Value < valorPagamentoCents) @@ -368,7 +371,7 @@ private async Task ProcessarChargeReembolsadoAsync(str return ProcessarEventoResultado.Aplicado; } - private async Task ProcessarEstornoTreinadorAsync(PagamentoTreinador pagamento, CancellationToken ct) + private async Task ProcessarEstornoTreinadorAsync(PagamentoTreinador pagamento, long? amountRefundedCents, CancellationToken ct) { if (pagamento.Status == PagamentoStatus.Estornado) { @@ -383,6 +386,19 @@ private async Task ProcessarEstornoTreinadorAsync(Paga return ProcessarEventoResultado.JaConsistente; } + if (!amountRefundedCents.HasValue) + throw new InvalidOperationException($"charge.refunded para PagamentoTreinador {pagamento.StripePaymentIntentId} sem amount_refunded. Retry necessário."); + + var valorPagamentoCents = (long)Math.Round(pagamento.Valor * 100m, MidpointRounding.AwayFromZero); + if (amountRefundedCents.Value < valorPagamentoCents) + { + logger.LogInformation( + "charge.refunded parcial para PagamentoTreinador {PaymentIntentId}: " + + "refunded={RefundedCents} < total={TotalCents}. Status mantido como Pago.", + pagamento.StripePaymentIntentId, amountRefundedCents.Value, valorPagamentoCents); + return ProcessarEventoResultado.JaConsistente; + } + var agora = timeProvider.GetUtcNow().UtcDateTime; var result = pagamento.MarcarEstornado(agora); if (result.IsFailure) @@ -435,6 +451,9 @@ private async Task ProcessarDisputaCriadaAsync(string? return ProcessarEventoResultado.JaConsistente; } + if (string.IsNullOrEmpty(disputeId)) + throw new InvalidOperationException($"charge.dispute.created para PaymentIntent {paymentIntentId} sem dispute id. Retry necessário."); + var agoraDisputa = timeProvider.GetUtcNow().UtcDateTime; var marcarDisputaResult = pagamento.MarcarEmDisputa(motivoDisputa ?? "unknown", agoraDisputa); if (marcarDisputaResult.IsFailure) @@ -501,6 +520,9 @@ private async Task ProcessarDisputaTreinadorAsync(Paga return ProcessarEventoResultado.JaConsistente; } + if (string.IsNullOrEmpty(disputeId)) + throw new InvalidOperationException($"charge.dispute.created para PagamentoTreinador {pagamento.StripePaymentIntentId} sem dispute id. Retry necessário."); + var agora = timeProvider.GetUtcNow().UtcDateTime; var result = pagamento.MarcarEmDisputa(agora); if (result.IsFailure) diff --git a/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/StripeWebhookParser.cs b/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/StripeWebhookParser.cs index 1f39ea81..ed3c008a 100644 --- a/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/StripeWebhookParser.cs +++ b/forzion.tech.Application/UseCases/Pagamentos/ProcessarWebhookStripe/StripeWebhookParser.cs @@ -19,7 +19,7 @@ public static StripeWebhookEvento Parse(string payload) var root = JsonNode.Parse(payload) ?? throw new InvalidOperationException("Payload do webhook inválido."); - var type = root["type"]?.GetValue() ?? string.Empty; + var type = TryGetStringValue(root["type"]) ?? string.Empty; var data = root["data"]?["object"]; // charge.refunded: data.object é Charge — `payment_intent` aponta pro PI subjacente. @@ -37,7 +37,7 @@ _ when type.StartsWith("payment_intent.", StringComparison.Ordinal) => TryGetStr var accountId = TryGetStringValue(root["account"]); var chargesEnabled = type == "account.updated" && - (data?["charges_enabled"]?.GetValue() ?? false); + (TryGetBoolValue(data?["charges_enabled"]) ?? false); // G-PAY-5: distingue refund total vs parcial — só refund total muda status. var amountRefundedCents = type == "charge.refunded" @@ -77,4 +77,11 @@ _ when type.StartsWith("payment_intent.", StringComparison.Ordinal) => TryGetStr return l; return null; } + + private static bool? TryGetBoolValue(JsonNode? node) + { + if (node is JsonValue jsonValue && jsonValue.TryGetValue(out var b)) + return b; + return null; + } } diff --git a/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeHandler.cs b/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeHandler.cs index 0d6bad3c..887b498c 100644 --- a/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeHandler.cs +++ b/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeHandler.cs @@ -1,4 +1,6 @@ using forzion.tech.Application.Interfaces; +using forzion.tech.Application.Interfaces.Repositories; +using forzion.tech.Domain.Entities; using forzion.tech.Domain.Shared; using forzion.tech.Application.UseCases.Pagamentos.ProcessarWebhookStripe; using Microsoft.Extensions.DependencyInjection; @@ -7,16 +9,19 @@ namespace forzion.tech.Application.UseCases.Pagamentos.ReconciliarPagamentosStripe; /// -/// Safety-net contra webhooks Stripe perdidos: poll Events.List dos últimos -/// N dias e reprocessa cada evento via . +/// Safety-net contra webhooks Stripe perdidos: poll Events.List a partir de um cursor +/// persistido (high-water-mark) e reprocessa cada evento via +/// . /// /// Idempotência é garantida pelos próprios handlers (checks de Status != Pendente /// e OnboardingCompleto) — replays seguros mesmo quando o webhook chegou e o evento -/// também foi pego pelo polling. +/// também foi pego pelo polling. Assinatura NÃO é validada: eventos vêm autenticados pela secret key. /// /// -/// Assinatura Stripe NÃO é validada aqui — eventos vêm direto da API autenticados pela -/// nossa secret key, então não há vetor de spoofing como existe no endpoint público. +/// O cursor avança incrementalmente (, +/// monotônico) conforme os eventos são processados, então um crash no meio do catch-up não +/// re-varre o já-processado. Truncamento (cap de batch) sinaliza backlog restante sem perda: +/// o cursor só passa do último evento processado. /// /// public class ReconciliarPagamentosStripeHandler( @@ -25,7 +30,13 @@ public class ReconciliarPagamentosStripeHandler( TimeProvider timeProvider, ILogger logger) { - private static readonly TimeSpan JanelaPadrao = TimeSpan.FromDays(7); + private static readonly TimeSpan JanelaMaxInicial = TimeSpan.FromDays(7); + private const int LotePersistenciaCursor = 100; + + // account.updated de conta conectada NÃO volta no Events.List da plataforma (vem por Connect + // webhook) — por isso a confirmação de onboarding é por poll Account.GetAsync, não por evento. + // Cap: 1 GET Stripe por conta pendente, limita rate-limit. + private const int MaxContasOnboardingPorRun = 200; public virtual async Task> HandleAsync( ReconciliarPagamentosStripeCommand command, @@ -33,17 +44,37 @@ public virtual async Task> HandleAsy { ArgumentNullException.ThrowIfNull(command); - var desde = command.DesdeUtc ?? timeProvider.GetUtcNow().UtcDateTime.Subtract(JanelaPadrao); + var agora = timeProvider.GetUtcNow().UtcDateTime; + var janelaMax = agora.Subtract(JanelaMaxInicial); + + using var cursorScope = scopeFactory.CreateScope(); + var cursorRepo = cursorScope.ServiceProvider.GetRequiredService(); + var cursorUow = cursorScope.ServiceProvider.GetRequiredService(); + + var estado = await cursorRepo.ObterAsync(cancellationToken).ConfigureAwait(false); + var cursorUtc = estado?.UltimoEventoReconciliadoUtc; + + var desde = command.DesdeUtc + ?? (cursorUtc is { } c && c > janelaMax ? c : janelaMax); logger.LogInformation("Iniciando reconciliação Stripe a partir de {DesdeUtc:o}.", desde); - var eventos = await stripeService.ListarEventosDesdeAsync(desde, cancellationToken).ConfigureAwait(false); + var lote = await stripeService.ListarEventosDesdeAsync(desde, cancellationToken).ConfigureAwait(false); var replayed = 0; var jaConsistentes = 0; var erros = 0; + var desdeUltimaPersistencia = 0; + + async Task PersistirCursorAsync(DateTime ate) + { + estado ??= ReconciliacaoStripeEstado.Criar(ate, agora); + estado.AvancarCursor(ate, agora); + await cursorRepo.SalvarAsync(estado, cancellationToken).ConfigureAwait(false); + await cursorUow.CommitAsync(cancellationToken).ConfigureAwait(false); + } - foreach (var evt in eventos) + foreach (var evt in lote.Eventos) { cancellationToken.ThrowIfCancellationRequested(); @@ -61,8 +92,6 @@ public virtual async Task> HandleAsy } else { - // JaConsistente (idempotência/alvo ausente/cross-account rejeitado) e - // Ignorado (tipo fora do escopo) somam no mesmo bucket — não há ação útil. jaConsistentes++; } } @@ -77,17 +106,77 @@ public virtual async Task> HandleAsy erros++; logger.LogError(ex, "Falha ao reprocessar evento Stripe {EventId} ({EventType}).", evt.EventId, evt.Type); } + + if (++desdeUltimaPersistencia >= LotePersistenciaCursor) + { + await PersistirCursorAsync(evt.Created).ConfigureAwait(false); + desdeUltimaPersistencia = 0; + } } + if (lote.Eventos.Count > 0) + await PersistirCursorAsync(lote.Eventos[^1].Created).ConfigureAwait(false); + + var onboardingConfirmados = await ReconciliarOnboardingConnectAsync(cancellationToken).ConfigureAwait(false); + logger.LogInformation( - "Reconciliação concluída: total={Total} replayed={Replayed} jaConsistentes={JaConsistentes} erros={Erros}.", - eventos.Count, replayed, jaConsistentes, erros); + "Reconciliação concluída: total={Total} replayed={Replayed} jaConsistentes={JaConsistentes} erros={Erros} truncado={Truncado} onboardingConfirmados={Onboarding}.", + lote.Eventos.Count, replayed, jaConsistentes, erros, lote.Truncado, onboardingConfirmados); return Result.Success(new ReconciliarPagamentosStripeResponse( - TotalEventos: eventos.Count, + TotalEventos: lote.Eventos.Count, Replayed: replayed, JaConsistentes: jaConsistentes, Erros: erros, - DesdeUtc: desde)); + DesdeUtc: desde, + Truncado: lote.Truncado, + OnboardingConfirmados: onboardingConfirmados)); + } + + private async Task ReconciliarOnboardingConnectAsync(CancellationToken cancellationToken) + { + using var scope = scopeFactory.CreateScope(); + var contaRepo = scope.ServiceProvider.GetRequiredService(); + var pendentes = await contaRepo + .ListarConfiguradasPendentesOnboardingAsync(MaxContasOnboardingPorRun, cancellationToken) + .ConfigureAwait(false); + + var confirmados = 0; + + foreach (var conta in pendentes) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var ativada = await stripeService + .ContaEstaAtivadaAsync(conta.StripeConnectAccountId!, cancellationToken) + .ConfigureAwait(false); + if (!ativada) continue; + + var evento = new StripeWebhookEvento("account.updated", null, conta.StripeConnectAccountId, ChargesEnabled: true); + using var evtScope = scopeFactory.CreateScope(); + var webhookHandler = evtScope.ServiceProvider.GetRequiredService(); + var resultado = await webhookHandler.ProcessarEventoAsync(evento, cancellationToken).ConfigureAwait(false); + + if (resultado == ProcessarEventoResultado.Aplicado) + { + confirmados++; + logger.LogInformation("Onboarding Connect confirmado via reconciliação para treinador {TreinadorId}.", conta.TreinadorId); + } + } + catch (OperationCanceledException) + { + throw; + } +#pragma warning disable CA1031 // falha de uma conta isolada não aborta o restante da varredura + catch (Exception ex) +#pragma warning restore CA1031 + { + logger.LogError(ex, "Falha ao reconciliar onboarding da conta Connect {AccountId}.", conta.StripeConnectAccountId); + } + } + + return confirmados; } } diff --git a/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeResponse.cs b/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeResponse.cs index 8c397ac7..f13c2b9f 100644 --- a/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeResponse.cs +++ b/forzion.tech.Application/UseCases/Pagamentos/ReconciliarPagamentosStripe/ReconciliarPagamentosStripeResponse.cs @@ -1,14 +1,19 @@ namespace forzion.tech.Application.UseCases.Pagamentos.ReconciliarPagamentosStripe; /// -/// Resultado agregado da reconciliação. TotalEventos = todos eventos retornados pela -/// Stripe Events.List dentro da janela; Replayed = mudaram estado; +/// Resultado agregado da reconciliação. TotalEventos = eventos retornados pela +/// Stripe Events.List dentro do batch; Replayed = mudaram estado; /// JaConsistentes = no-op (idempotência ou alvo não localizado); -/// Erros = exceções capturadas durante replay (logadas, varredura prossegue). +/// Erros = exceções capturadas durante replay (logadas, varredura prossegue); +/// Truncado = cap de batch atingido, há backlog restante (cron deve re-disparar); +/// OnboardingConfirmados = contas Connect ativadas via poll Account.GetAsync +/// (eventos account.updated de conta conectada não voltam no Events.List da plataforma). /// public sealed record ReconciliarPagamentosStripeResponse( int TotalEventos, int Replayed, int JaConsistentes, int Erros, - DateTime DesdeUtc); + DateTime DesdeUtc, + bool Truncado, + int OnboardingConfirmados); diff --git a/forzion.tech.Domain/Entities/ReconciliacaoStripeEstado.cs b/forzion.tech.Domain/Entities/ReconciliacaoStripeEstado.cs new file mode 100644 index 00000000..fd27c5f0 --- /dev/null +++ b/forzion.tech.Domain/Entities/ReconciliacaoStripeEstado.cs @@ -0,0 +1,27 @@ +namespace forzion.tech.Domain.Entities; + +public class ReconciliacaoStripeEstado +{ + public Guid Id { get; private set; } + public DateTime UltimoEventoReconciliadoUtc { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + + private ReconciliacaoStripeEstado() { } + + public static ReconciliacaoStripeEstado Criar(DateTime ultimoEventoReconciliadoUtc, DateTime agora) => new() + { + Id = Guid.NewGuid(), + UltimoEventoReconciliadoUtc = ultimoEventoReconciliadoUtc, + CreatedAt = agora, + }; + + public void AvancarCursor(DateTime ateUtc, DateTime agora) + { + if (ateUtc <= UltimoEventoReconciliadoUtc) + return; + + UltimoEventoReconciliadoUtc = ateUtc; + UpdatedAt = agora; + } +} diff --git a/forzion.tech.Infrastructure/DependencyInjection/InfrastructureExtensions.cs b/forzion.tech.Infrastructure/DependencyInjection/InfrastructureExtensions.cs index a16e0c4b..61c40622 100644 --- a/forzion.tech.Infrastructure/DependencyInjection/InfrastructureExtensions.cs +++ b/forzion.tech.Infrastructure/DependencyInjection/InfrastructureExtensions.cs @@ -119,6 +119,7 @@ public static IServiceCollection AddInfrastructure( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -135,6 +136,10 @@ public static IServiceCollection AddInfrastructure( "Stripe:WebhookSecret não configurado. Use User Secrets ou variável de ambiente.") .Validate(s => s.TaxaPlataformaPercent > 0 && s.TaxaPlataformaPercent <= 100, "Stripe:TaxaPlataformaPercent deve estar entre 0 e 100.") + .Validate(s => !(s.SecretKey.StartsWith("sk_live_", StringComparison.Ordinal) && s.ExpectLivemode != true), + "Stripe:SecretKey usa prefixo sk_live_ mas Stripe__ExpectLivemode não é true. Defina Stripe__ExpectLivemode=true em produção.") + .Validate(s => !(s.SecretKey.StartsWith("sk_test_", StringComparison.Ordinal) && s.ExpectLivemode == true), + "Stripe:SecretKey usa prefixo sk_test_ mas Stripe__ExpectLivemode é true. Não use chave test-mode em produção.") .ValidateOnStart(); // SEC-03: se ExpectLivemode não foi configurado explicitamente, default por ambiente — // Production espera live; demais (incl. Homolog público em test-mode) não enforça. @@ -203,6 +208,12 @@ public static IServiceCollection AddInfrastructure( }); services.AddSingleton(); + services.AddOptions() + .BindConfiguration("Internal") + .Validate(s => !isProduction || !string.IsNullOrWhiteSpace(s.ApiKey), + "Internal:ApiKey não configurado. O endpoint interno de cron ficaria sem proteção em produção.") + .ValidateOnStart(); + // PaymentSettings — expõe taxa de plataforma para a camada Application services.AddOptions() .Configure>((payment, stripe) => diff --git a/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.Designer.cs b/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.Designer.cs new file mode 100644 index 00000000..a836ea46 --- /dev/null +++ b/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.Designer.cs @@ -0,0 +1,2674 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using forzion.tech.Infrastructure.Persistence; + +#nullable disable + +namespace forzion.tech.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260626152551_CriarReconciliacaoStripeEstado")] + partial class CriarReconciliacaoStripeEstado + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Aluno", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Anonimizado") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("anonimizado"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DiasDisponiveis") + .HasColumnType("integer") + .HasColumnName("dias_disponiveis"); + + b.Property("Doencas") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("doencas"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("Finalidade") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("finalidade"); + + b.Property("FocoTreino") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("foco_treino"); + + b.Property("LimitacoesFisicas") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("limitacoes_fisicas"); + + b.Property("NivelCondicionamento") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("nivel_condicionamento"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("ObservacoesAdicionais") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("observacoes_adicionais"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("Telefone") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("telefone"); + + b.Property("TempoDisponivelMinutos") + .HasColumnType("integer") + .HasColumnName("tempo_disponivel_minutos"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_alunos"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_alunos_conta_id"); + + b.HasIndex("Status") + .HasDatabaseName("ix_alunos_status"); + + b.ToTable("alunos", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Assinante", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AlunoId") + .HasColumnType("uuid") + .HasColumnName("aluno_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_assinantes"); + + b.HasIndex("AlunoId") + .IsUnique() + .HasDatabaseName("ix_assinantes_aluno_id"); + + b.ToTable("assinantes", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.AssinaturaAluno", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AlunoId") + .HasColumnType("uuid") + .HasColumnName("aluno_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataCancelamento") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_cancelamento"); + + b.Property("DataInicio") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_inicio"); + + b.Property("DataProximaCobranca") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_proxima_cobranca"); + + b.Property("PacoteId") + .HasColumnType("uuid") + .HasColumnName("pacote_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("TentativasFalhasConsecutivas") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("tentativas_falhas_consecutivas"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Valor") + .HasColumnType("numeric(10,2)") + .HasColumnName("valor"); + + b.Property("VinculoId") + .HasColumnType("uuid") + .HasColumnName("vinculo_id"); + + b.HasKey("Id") + .HasName("pk_assinaturas_aluno"); + + b.HasIndex("AlunoId") + .HasDatabaseName("ix_assinaturas_aluno_aluno_id"); + + b.HasIndex("PacoteId") + .HasDatabaseName("ix_assinaturas_aluno_pacote_id"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_assinaturas_aluno_treinador_id"); + + b.HasIndex("VinculoId") + .IsUnique() + .HasDatabaseName("ix_assinaturas_aluno_vinculo_id"); + + b.HasIndex("Status", "DataProximaCobranca") + .HasDatabaseName("ix_assinaturas_aluno_status_data_proxima_cobranca"); + + b.ToTable("assinaturas_aluno", null, t => + { + t.HasCheckConstraint("ck_assinaturas_aluno_valor_nao_negativo", "\"valor\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.AssinaturaTreinador", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataCancelamento") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_cancelamento"); + + b.Property("DataInicio") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_inicio"); + + b.Property("DataProximaCobranca") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_proxima_cobranca"); + + b.Property("PlanoPlataformaId") + .HasColumnType("uuid") + .HasColumnName("plano_plataforma_id"); + + b.Property("PlanoPlataformaIdAgendado") + .HasColumnType("uuid") + .HasColumnName("plano_plataforma_id_agendado"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("TentativasFalhasConsecutivas") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("tentativas_falhas_consecutivas"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Valor") + .HasColumnType("numeric(10,2)") + .HasColumnName("valor"); + + b.HasKey("Id") + .HasName("pk_assinaturas_treinador"); + + b.HasIndex("PlanoPlataformaId") + .HasDatabaseName("ix_assinaturas_treinador_plano_plataforma_id"); + + b.HasIndex("PlanoPlataformaIdAgendado") + .HasDatabaseName("ix_assinaturas_treinador_plano_plataforma_id_agendado"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_assinaturas_treinador_treinador_id"); + + b.HasIndex("Status", "DataProximaCobranca") + .HasDatabaseName("ix_assinaturas_treinador_status_data_proxima_cobranca"); + + b.ToTable("assinaturas_treinador", null, t => + { + t.HasCheckConstraint("ck_assinaturas_treinador_valor_nao_negativo", "\"valor\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Conta", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AnonimizadaEm") + .HasColumnType("timestamptz") + .HasColumnName("anonimizada_em"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailVerificado") + .HasColumnType("boolean") + .HasColumnName("email_verificado"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("SessoesInvalidasAntesDeUtc") + .HasColumnType("timestamptz") + .HasColumnName("sessoes_invalidas_antes_de_utc"); + + b.Property("TipoConta") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tipo_conta"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerificadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("verificado_em"); + + b.HasKey("Id") + .HasName("pk_contas"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_contas_email"); + + b.ToTable("contas", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ContaMfa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AtualizadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("atualizado_em"); + + b.Property("ConfirmadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("confirmado_em"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("Habilitado") + .HasColumnType("boolean") + .HasColumnName("habilitado"); + + b.Property("TotpSecretCifrado") + .HasColumnType("text") + .HasColumnName("totp_secret_cifrado"); + + b.Property("UltimoTimeStep") + .HasColumnType("bigint") + .HasColumnName("ultimo_time_step"); + + b.HasKey("Id") + .HasName("pk_conta_mfa"); + + b.HasIndex("ContaId") + .IsUnique() + .HasDatabaseName("ix_conta_mfa_conta_id"); + + b.ToTable("conta_mfa", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ContaRecebimento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("OnboardingCompleto") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("onboarding_completo"); + + b.Property("StripeConnectAccountId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("stripe_connect_account_id"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_conta_recebimento"); + + b.HasIndex("StripeConnectAccountId") + .HasDatabaseName("ix_conta_recebimento_stripe_connect_account_id"); + + b.HasIndex("TreinadorId") + .IsUnique() + .HasDatabaseName("ix_conta_recebimento_treinador_id"); + + b.ToTable("conta_recebimento", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.EmailDeliveryLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("event_type"); + + b.Property("OcorridoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("ocorrido_em"); + + b.Property("RecipientEmailHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("recipient_email_hash"); + + b.Property("ResendMessageId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("resend_message_id"); + + b.HasKey("Id") + .HasName("pk_email_delivery_logs"); + + b.HasIndex("EventType") + .HasDatabaseName("ix_email_delivery_logs_event_type"); + + b.HasIndex("ResendMessageId", "EventType") + .IsUnique() + .HasDatabaseName("ix_email_delivery_logs_resend_message_id_event_type"); + + b.ToTable("email_delivery_logs", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.EmailVerificationToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_email_verification_tokens"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_email_verification_tokens_conta_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_email_verification_tokens_token_hash"); + + b.ToTable("email_verification_tokens", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ErrorLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Mensagem") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("mensagem"); + + b.Property("Nivel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("nivel"); + + b.Property("OcorridoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("ocorrido_em"); + + b.Property("Origem") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("origem"); + + b.HasKey("Id") + .HasName("pk_error_logs"); + + b.HasIndex("OcorridoEm") + .HasDatabaseName("ix_error_logs_ocorrido_em"); + + b.ToTable("error_logs", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ExecucaoExercicio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CargaExecutada") + .HasColumnType("numeric(10,2)") + .HasColumnName("carga_executada"); + + b.Property("ExecucaoTreinoId") + .HasColumnType("uuid") + .HasColumnName("execucao_treino_id"); + + b.Property("Observacao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("observacao"); + + b.Property("RepeticoesExecutadas") + .HasColumnType("integer") + .HasColumnName("repeticoes_executadas"); + + b.Property("SeriesExecutadas") + .HasColumnType("integer") + .HasColumnName("series_executadas"); + + b.Property("TreinoExercicioId") + .HasColumnType("uuid") + .HasColumnName("treino_exercicio_id"); + + b.HasKey("Id") + .HasName("pk_execucoes_exercicio"); + + b.HasIndex("ExecucaoTreinoId") + .HasDatabaseName("ix_execucoes_exercicio_execucao_treino_id"); + + b.HasIndex("TreinoExercicioId") + .HasDatabaseName("ix_execucoes_exercicio_treino_exercicio_id"); + + b.ToTable("execucoes_exercicio", null, t => + { + t.HasCheckConstraint("ck_execucoes_exercicio_repeticoes_positivo", "\"repeticoes_executadas\" > 0"); + + t.HasCheckConstraint("ck_execucoes_exercicio_series_positivo", "\"series_executadas\" > 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ExecucaoTreino", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AlunoId") + .HasColumnType("uuid") + .HasColumnName("aluno_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataExecucao") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_execucao"); + + b.Property("IdempotencyKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("idempotency_key"); + + b.Property("Observacao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("observacao"); + + b.Property("TreinoId") + .HasColumnType("uuid") + .HasColumnName("treino_id"); + + b.HasKey("Id") + .HasName("pk_execucoes_treino"); + + b.HasIndex("AlunoId") + .HasDatabaseName("ix_execucoes_treino_aluno_id"); + + b.HasIndex("TreinoId") + .HasDatabaseName("ix_execucoes_treino_treino_id"); + + b.HasIndex("AlunoId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_execucoes_treino_aluno_id_idempotency_key_unique") + .HasFilter("idempotency_key IS NOT NULL"); + + b.ToTable("execucoes_treino", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Exercicio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ComoExecutar") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("como_executar"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Descricao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("descricao"); + + b.Property("GrupoMuscularId") + .HasColumnType("uuid") + .HasColumnName("grupo_muscular_id"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VideoId") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("video_id"); + + b.HasKey("Id") + .HasName("pk_exercicios"); + + b.HasIndex("GrupoMuscularId") + .HasDatabaseName("ix_exercicios_grupo_muscular_id"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_exercicios_treinador_id"); + + b.ToTable("exercicios", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.GrupoMuscular", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("nome"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_grupos_musculares"); + + b.HasIndex("Nome") + .IsUnique() + .HasDatabaseName("ix_grupos_musculares_nome"); + + b.ToTable("grupos_musculares", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.HealthReportConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Ativo") + .HasColumnType("boolean") + .HasColumnName("ativo"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Destinatarios") + .IsRequired() + .HasColumnType("text") + .HasColumnName("destinatarios"); + + b.Property("HoraEnvioUtc") + .HasColumnType("time without time zone") + .HasColumnName("hora_envio_utc"); + + b.Property("IncluirEntregabilidade") + .HasColumnType("boolean") + .HasColumnName("incluir_entregabilidade"); + + b.Property("IncluirErros") + .HasColumnType("boolean") + .HasColumnName("incluir_erros"); + + b.Property("IncluirKpis") + .HasColumnType("boolean") + .HasColumnName("incluir_kpis"); + + b.Property("IncluirLiveness") + .HasColumnType("boolean") + .HasColumnName("incluir_liveness"); + + b.Property("UltimoEnvioEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("ultimo_envio_em"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_health_report_config"); + + b.ToTable("health_report_config", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.HealthSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Ambiente") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("ambiente"); + + b.Property("CapturadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("capturado_em"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("payload_json"); + + b.Property("StatusGeral") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status_geral"); + + b.HasKey("Id") + .HasName("pk_health_snapshots"); + + b.HasIndex("CapturadoEm") + .HasDatabaseName("ix_health_snapshots_capturado_em"); + + b.ToTable("health_snapshots", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.LogAprovacao", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EntidadeId") + .HasColumnType("uuid") + .HasColumnName("entidade_id"); + + b.Property("EntidadeTipo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("entidade_tipo"); + + b.Property("Observacao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("observacao"); + + b.Property("RealizadoPorId") + .HasColumnType("uuid") + .HasColumnName("realizado_por_id"); + + b.Property("TipoAcao") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tipo_acao"); + + b.HasKey("Id") + .HasName("pk_logs_aprovacao"); + + b.HasIndex("EntidadeId") + .HasDatabaseName("ix_logs_aprovacao_entidade_id"); + + b.HasIndex("RealizadoPorId") + .HasDatabaseName("ix_logs_aprovacao_realizado_por_id"); + + b.ToTable("logs_aprovacao", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.MensagemSuporte", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Assunto") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("assunto"); + + b.Property("Categoria") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("categoria"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadaEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criada_em"); + + b.Property("Descricao") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("descricao"); + + b.HasKey("Id") + .HasName("pk_mensagens_suporte"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_mensagens_suporte_conta_id"); + + b.ToTable("mensagens_suporte", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.MfaChallenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CodigoHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("codigo_hash"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("ExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("expira_em"); + + b.Property("Proposito") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("proposito"); + + b.Property("Tentativas") + .HasColumnType("integer") + .HasColumnName("tentativas"); + + b.Property("UsadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("usado_em"); + + b.HasKey("Id") + .HasName("pk_mfa_challenges"); + + b.HasIndex("ContaId", "Proposito") + .HasDatabaseName("ix_mfa_challenges_conta_id_proposito"); + + b.ToTable("mfa_challenges", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.MfaRecoveryCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CodigoHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("codigo_hash"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("UsadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("usado_em"); + + b.HasKey("Id") + .HasName("pk_mfa_recovery_codes"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_mfa_recovery_codes_conta_id"); + + b.ToTable("mfa_recovery_codes", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.NotaFiscal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CancelamentoPendentePreEmissao") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("cancelamento_pendente_pre_emissao"); + + b.Property("ChaveAcesso") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("chave_acesso"); + + b.Property("CodigoErro") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("codigo_erro"); + + b.Property("CompetenciaFim") + .HasColumnType("date") + .HasColumnName("competencia_fim"); + + b.Property("CompetenciaInicio") + .HasColumnType("date") + .HasColumnName("competencia_inicio"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DanfseRef") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("danfse_ref"); + + b.Property("DataEmissao") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_emissao"); + + b.Property("MotivoCancelamentoPendente") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("motivo_cancelamento_pendente"); + + b.Property("MotivoErro") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("motivo_erro"); + + b.Property("NumeroDps") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("numero_dps"); + + b.Property("NumeroNfse") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("numero_nfse"); + + b.Property("PagamentoTreinadorId") + .HasColumnType("uuid") + .HasColumnName("pagamento_treinador_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("status"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("tipo"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Valor") + .HasColumnType("numeric(10,2)") + .HasColumnName("valor"); + + b.HasKey("Id") + .HasName("pk_notas_fiscais"); + + b.HasIndex("PagamentoTreinadorId") + .IsUnique() + .HasDatabaseName("ix_notas_fiscais_pagamento_treinador_id_unique") + .HasFilter("pagamento_treinador_id IS NOT NULL"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_notas_fiscais_treinador_id"); + + b.HasIndex("TreinadorId", "Tipo", "CompetenciaInicio") + .IsUnique() + .HasDatabaseName("ix_notas_fiscais_treinador_tipo_competencia_unique") + .HasFilter("competencia_inicio IS NOT NULL"); + + b.ToTable("notas_fiscais", null, t => + { + t.HasCheckConstraint("ck_notas_fiscais_valor_nao_negativo", "\"valor\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.OutboxEfeito", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ChaveIdempotencia") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("chave_idempotencia"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("ProcessadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("processado_em"); + + b.Property("ProximaTentativa") + .HasColumnType("timestamp with time zone") + .HasColumnName("proxima_tentativa"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("Tentativas") + .HasColumnType("integer") + .HasColumnName("tentativas"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("tipo"); + + b.Property("UltimoErro") + .HasColumnType("text") + .HasColumnName("ultimo_erro"); + + b.HasKey("Id") + .HasName("pk_outbox_efeitos"); + + b.HasIndex("ChaveIdempotencia") + .IsUnique() + .HasDatabaseName("ix_outbox_efeitos_chave_idempotencia_unique"); + + b.HasIndex("Status", "ProximaTentativa") + .HasDatabaseName("ix_outbox_efeitos_status_proxima_tentativa"); + + b.ToTable("outbox_efeitos", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Pacote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Descricao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("descricao"); + + b.Property("IsAtivo") + .HasColumnType("boolean") + .HasColumnName("is_ativo"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("Preco") + .HasColumnType("numeric(10,2)") + .HasColumnName("preco"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_pacotes"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_pacotes_treinador_id"); + + b.ToTable("pacotes", null, t => + { + t.HasCheckConstraint("ck_pacotes_preco_nao_negativo", "\"preco\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Pagamento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AssinaturaAlunoId") + .HasColumnType("uuid") + .HasColumnName("assinatura_aluno_id"); + + b.Property("ClientSecret") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("client_secret"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataPagamento") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_pagamento"); + + b.Property("MetodoPagamento") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Pix") + .HasColumnName("metodo_pagamento"); + + b.Property("PixExpiracao") + .HasColumnType("timestamp with time zone") + .HasColumnName("pix_expiracao"); + + b.Property("PixQrCode") + .HasColumnType("text") + .HasColumnName("pix_qr_code"); + + b.Property("PixQrCodeUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("pix_qr_code_url"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("StripePaymentIntentId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("stripe_payment_intent_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Valor") + .HasColumnType("numeric(10,2)") + .HasColumnName("valor"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_pagamentos"); + + b.HasIndex("AssinaturaAlunoId") + .IsUnique() + .HasDatabaseName("ix_pagamentos_assinatura_aluno_id_pendente_unique") + .HasFilter("status = 'Pendente'"); + + b.HasIndex("StripePaymentIntentId") + .IsUnique() + .HasDatabaseName("ix_pagamentos_stripe_payment_intent_id"); + + b.HasIndex("AssinaturaAlunoId", "Status") + .HasDatabaseName("ix_pagamentos_assinatura_aluno_id_status"); + + b.ToTable("pagamentos", null, t => + { + t.HasCheckConstraint("ck_pagamentos_valor_nao_negativo", "\"valor\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.PagamentoTreinador", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AssinaturaTreinadorId") + .HasColumnType("uuid") + .HasColumnName("assinatura_treinador_id"); + + b.Property("ClientSecret") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("client_secret"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataPagamento") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_pagamento"); + + b.Property("Finalidade") + .IsRequired() + .HasColumnType("text") + .HasColumnName("finalidade"); + + b.Property("MetodoPagamento") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Pix") + .HasColumnName("metodo_pagamento"); + + b.Property("PixExpiracao") + .HasColumnType("timestamp with time zone") + .HasColumnName("pix_expiracao"); + + b.Property("PixQrCode") + .HasColumnType("text") + .HasColumnName("pix_qr_code"); + + b.Property("PixQrCodeUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("pix_qr_code_url"); + + b.Property("PlanoAlvoId") + .HasColumnType("uuid") + .HasColumnName("plano_alvo_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("StripePaymentIntentId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("stripe_payment_intent_id"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Valor") + .HasColumnType("numeric(10,2)") + .HasColumnName("valor"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_pagamentos_treinador"); + + b.HasIndex("AssinaturaTreinadorId") + .IsUnique() + .HasDatabaseName("ix_pagamentos_treinador_assinatura_id_pendente_unique") + .HasFilter("status = 'Pendente'"); + + b.HasIndex("PlanoAlvoId") + .HasDatabaseName("ix_pagamentos_treinador_plano_alvo_id"); + + b.HasIndex("StripePaymentIntentId") + .IsUnique() + .HasDatabaseName("ix_pagamentos_treinador_stripe_payment_intent_id"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_pagamentos_treinador_treinador_id"); + + b.HasIndex("AssinaturaTreinadorId", "Status") + .HasDatabaseName("ix_pagamentos_treinador_assinatura_id_status"); + + b.ToTable("pagamentos_treinador", null, t => + { + t.HasCheckConstraint("ck_pagamentos_treinador_valor_nao_negativo", "\"valor\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.PasswordResetToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.HasKey("Id") + .HasName("pk_password_reset_tokens"); + + b.HasIndex("ContaId") + .IsUnique() + .HasDatabaseName("ux_password_reset_tokens_conta_id_pendente") + .HasFilter("used_at IS NULL"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_password_reset_tokens_token_hash"); + + b.ToTable("password_reset_tokens", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.PlanoPlataforma", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Descricao") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("descricao"); + + b.Property("IsAtivo") + .HasColumnType("boolean") + .HasColumnName("is_ativo"); + + b.Property("MaxAlunos") + .HasColumnType("integer") + .HasColumnName("max_alunos"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("Preco") + .HasColumnType("numeric(10,2)") + .HasColumnName("preco"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("tier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_planos_plataforma"); + + b.ToTable("planos_plataforma", null, t => + { + t.HasCheckConstraint("ck_planos_plataforma_max_alunos_positivo", "\"max_alunos\" > 0"); + + t.HasCheckConstraint("ck_planos_plataforma_preco_nao_negativo", "\"preco\" >= 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ReconciliacaoStripeEstado", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UltimoEventoReconciliadoUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("ultimo_evento_reconciliado_utc"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_reconciliacao_stripe_estado"); + + b.ToTable("reconciliacao_stripe_estado", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("ExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("expira_em"); + + b.Property("FamiliaId") + .HasColumnType("uuid") + .HasColumnName("familia_id"); + + b.Property("SubstituidoPorId") + .HasColumnType("uuid") + .HasColumnName("substituido_por_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UsadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("usado_em"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("FamiliaId") + .HasDatabaseName("ix_refresh_tokens_familia_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_refresh_tokens_token_hash"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.RefreshTokenFamily", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AbsolutoExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("absoluto_expira_em"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadaEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criada_em"); + + b.Property("MotivoRevogacao") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("motivo_revogacao"); + + b.Property("RevogadaEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("revogada_em"); + + b.Property("Rotulo") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("rotulo"); + + b.HasKey("Id") + .HasName("pk_refresh_token_families"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_refresh_token_families_conta_id"); + + b.HasIndex("RevogadaEm") + .HasDatabaseName("ix_refresh_token_families_revogada_em"); + + b.ToTable("refresh_token_families", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.SerieConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Carga") + .HasColumnType("numeric(10,2)") + .HasColumnName("carga"); + + b.Property("Descanso") + .HasColumnType("integer") + .HasColumnName("descanso"); + + b.Property("Descricao") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("descricao"); + + b.Property("Ordem") + .HasColumnType("integer") + .HasColumnName("ordem"); + + b.Property("Quantidade") + .HasColumnType("integer") + .HasColumnName("quantidade"); + + b.Property("RepeticoesMax") + .HasColumnType("integer") + .HasColumnName("repeticoes_max"); + + b.Property("RepeticoesMin") + .HasColumnType("integer") + .HasColumnName("repeticoes_min"); + + b.Property("TreinoExercicioId") + .HasColumnType("uuid") + .HasColumnName("treino_exercicio_id"); + + b.HasKey("Id") + .HasName("pk_treino_exercicio_series"); + + b.HasIndex("TreinoExercicioId") + .HasDatabaseName("ix_treino_exercicio_series_treino_exercicio_id"); + + b.ToTable("treino_exercicio_series", null, t => + { + t.HasCheckConstraint("ck_treino_exercicio_series_quantidade_positivo", "\"quantidade\" > 0"); + + t.HasCheckConstraint("ck_treino_exercicio_series_repeticoes_max_gte_min", "\"repeticoes_max\" IS NULL OR \"repeticoes_max\" >= \"repeticoes_min\""); + + t.HasCheckConstraint("ck_treino_exercicio_series_repeticoes_min_positivo", "\"repeticoes_min\" > 0"); + }); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.SystemUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_system_users"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_system_users_conta_id"); + + b.ToTable("system_users", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TokenRevogado", b => + { + b.Property("Jti") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("jti"); + + b.Property("ExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("expira_em"); + + b.HasKey("Jti") + .HasName("pk_tokens_revogados"); + + b.HasIndex("ExpiraEm") + .HasDatabaseName("ix_tokens_revogados_expira_em"); + + b.ToTable("tokens_revogados", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Treinador", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Anonimizado") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("anonimizado"); + + b.Property("AprovadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("aprovado_em"); + + b.Property("AprovadoPorId") + .HasColumnType("uuid") + .HasColumnName("aprovado_por_id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModoPagamentoAluno") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Plataforma") + .HasColumnName("modo_pagamento_aluno"); + + b.Property("ModoPagamentoAlunoAlteradoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("modo_pagamento_aluno_alterado_em"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("PlanoPlataformaId") + .HasColumnType("uuid") + .HasColumnName("plano_plataforma_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("Telefone") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("telefone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_treinadores"); + + b.HasIndex("ContaId") + .IsUnique() + .HasDatabaseName("ix_treinadores_conta_id"); + + b.HasIndex("PlanoPlataformaId") + .HasDatabaseName("ix_treinadores_plano_plataforma_id"); + + b.HasIndex("Status") + .HasDatabaseName("ix_treinadores_status"); + + b.ToTable("treinadores", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Treino", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataFim") + .HasColumnType("date") + .HasColumnName("data_fim"); + + b.Property("DataInicio") + .HasColumnType("date") + .HasColumnName("data_inicio"); + + b.Property("Dificuldade") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Iniciante") + .HasColumnName("dificuldade"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nome"); + + b.Property("Objetivo") + .IsRequired() + .HasColumnType("text") + .HasColumnName("objetivo"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_treinos"); + + b.HasIndex("TreinadorId") + .HasDatabaseName("ix_treinos_treinador_id"); + + b.ToTable("treinos", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TreinoAluno", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AlunoId") + .HasColumnType("uuid") + .HasColumnName("aluno_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("TreinoId") + .HasColumnType("uuid") + .HasColumnName("treino_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_treino_alunos"); + + b.HasIndex("AlunoId") + .HasDatabaseName("ix_treino_alunos_aluno_id"); + + b.HasIndex("TreinoId") + .IsUnique() + .HasDatabaseName("ix_treino_alunos_treino_id") + .HasFilter("status = 'Ativo'"); + + b.ToTable("treino_alunos", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TreinoExercicio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ExercicioId") + .HasColumnType("uuid") + .HasColumnName("exercicio_id"); + + b.Property("Observacao") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("observacao"); + + b.Property("Ordem") + .HasColumnType("integer") + .HasColumnName("ordem"); + + b.Property("TreinoId") + .HasColumnType("uuid") + .HasColumnName("treino_id"); + + b.HasKey("Id") + .HasName("pk_treino_exercicios"); + + b.HasIndex("ExercicioId") + .HasDatabaseName("ix_treino_exercicios_exercicio_id"); + + b.HasIndex("TreinoId") + .HasDatabaseName("ix_treino_exercicios_treino_id"); + + b.ToTable("treino_exercicios", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TrocaEmailToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("ExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("expira_em"); + + b.Property("NovoEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("novo_email"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UsadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("usado_em"); + + b.HasKey("Id") + .HasName("pk_troca_email_tokens"); + + b.HasIndex("ContaId") + .HasDatabaseName("ix_troca_email_tokens_conta_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_troca_email_tokens_token_hash"); + + b.ToTable("troca_email_tokens", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TrustedDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContaId") + .HasColumnType("uuid") + .HasColumnName("conta_id"); + + b.Property("CriadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("criado_em"); + + b.Property("ExpiraEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("expira_em"); + + b.Property("RevogadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("revogado_em"); + + b.Property("Rotulo") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("rotulo"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UltimoUsoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("ultimo_uso_em"); + + b.HasKey("Id") + .HasName("pk_trusted_devices"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_trusted_devices_token_hash"); + + b.ToTable("trusted_devices", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.VinculoTreinadorAluno", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AlunoId") + .HasColumnType("uuid") + .HasColumnName("aluno_id"); + + b.Property("AprovadoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("aprovado_em"); + + b.Property("AprovadoPorId") + .HasColumnType("uuid") + .HasColumnName("aprovado_por_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DataFim") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_fim"); + + b.Property("DataInicio") + .HasColumnType("timestamp with time zone") + .HasColumnName("data_inicio"); + + b.Property("PacoteId") + .HasColumnType("uuid") + .HasColumnName("pacote_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("treinador_id"); + + b.HasKey("Id") + .HasName("pk_vinculos_treinador_aluno"); + + b.HasIndex("AlunoId") + .HasDatabaseName("ix_vinculos_treinador_aluno_aluno_id"); + + b.HasIndex("PacoteId") + .HasDatabaseName("ix_vinculos_treinador_aluno_pacote_id"); + + b.HasIndex("TreinadorId", "AlunoId") + .HasDatabaseName("ix_vinculos_treinador_aluno_treinador_id_aluno_id"); + + b.HasIndex("TreinadorId", "Status") + .HasDatabaseName("ix_vinculos_treinador_aluno_treinador_id_status"); + + b.ToTable("vinculos_treinador_aluno", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.WhatsAppDeliveryLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("event_type"); + + b.Property("MetaMessageId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("meta_message_id"); + + b.Property("OcorridoEm") + .HasColumnType("timestamp with time zone") + .HasColumnName("ocorrido_em"); + + b.Property("RecipientPhoneHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("recipient_phone_hash"); + + b.HasKey("Id") + .HasName("pk_whatsapp_delivery_logs"); + + b.HasIndex("EventType") + .HasDatabaseName("ix_whatsapp_delivery_logs_event_type"); + + b.HasIndex("MetaMessageId", "EventType") + .IsUnique() + .HasDatabaseName("ix_whatsapp_delivery_logs_meta_message_id_event_type"); + + b.ToTable("whatsapp_delivery_logs", (string)null); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Aluno", b => + { + b.HasOne("forzion.tech.Domain.Entities.Conta", null) + .WithMany() + .HasForeignKey("ContaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_alunos_contas_conta_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.AssinaturaAluno", b => + { + b.HasOne("forzion.tech.Domain.Entities.Aluno", null) + .WithMany() + .HasForeignKey("AlunoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_aluno_alunos_aluno_id"); + + b.HasOne("forzion.tech.Domain.Entities.Pacote", null) + .WithMany() + .HasForeignKey("PacoteId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_aluno_pacotes_pacote_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_aluno_treinadores_treinador_id"); + + b.HasOne("forzion.tech.Domain.Entities.VinculoTreinadorAluno", null) + .WithMany() + .HasForeignKey("VinculoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_aluno_vinculos_treinador_aluno_vinculo_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.AssinaturaTreinador", b => + { + b.HasOne("forzion.tech.Domain.Entities.PlanoPlataforma", null) + .WithMany() + .HasForeignKey("PlanoPlataformaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_treinador_planos_plataforma_plano_plataforma_id"); + + b.HasOne("forzion.tech.Domain.Entities.PlanoPlataforma", null) + .WithMany() + .HasForeignKey("PlanoPlataformaIdAgendado") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_assinaturas_treinador_planos_plataforma_plano_plataforma_id1"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assinaturas_treinador_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ContaRecebimento", b => + { + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_conta_recebimento_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ExecucaoExercicio", b => + { + b.HasOne("forzion.tech.Domain.Entities.ExecucaoTreino", null) + .WithMany("Exercicios") + .HasForeignKey("ExecucaoTreinoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_execucoes_exercicio_execucoes_treino_execucao_treino_id"); + + b.HasOne("forzion.tech.Domain.Entities.TreinoExercicio", null) + .WithMany() + .HasForeignKey("TreinoExercicioId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_execucoes_exercicio_treino_exercicios_treino_exercicio_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ExecucaoTreino", b => + { + b.HasOne("forzion.tech.Domain.Entities.Aluno", null) + .WithMany() + .HasForeignKey("AlunoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_execucoes_treino_alunos_aluno_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treino", null) + .WithMany() + .HasForeignKey("TreinoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_execucoes_treino_treinos_treino_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Exercicio", b => + { + b.HasOne("forzion.tech.Domain.Entities.GrupoMuscular", null) + .WithMany() + .HasForeignKey("GrupoMuscularId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exercicios_grupos_musculares_grupo_muscular_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exercicios_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.MensagemSuporte", b => + { + b.HasOne("forzion.tech.Domain.Entities.Conta", null) + .WithMany() + .HasForeignKey("ContaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_mensagens_suporte_contas_conta_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.NotaFiscal", b => + { + b.HasOne("forzion.tech.Domain.Entities.PagamentoTreinador", null) + .WithMany() + .HasForeignKey("PagamentoTreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_notas_fiscais_pagamentos_treinador_pagamento_treinador_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_notas_fiscais_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Pacote", b => + { + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_pacotes_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Pagamento", b => + { + b.HasOne("forzion.tech.Domain.Entities.AssinaturaAluno", null) + .WithMany() + .HasForeignKey("AssinaturaAlunoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_pagamentos_assinaturas_aluno_assinatura_aluno_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.PagamentoTreinador", b => + { + b.HasOne("forzion.tech.Domain.Entities.AssinaturaTreinador", null) + .WithMany() + .HasForeignKey("AssinaturaTreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_pagamentos_treinador_assinaturas_treinador_assinatura_trein"); + + b.HasOne("forzion.tech.Domain.Entities.PlanoPlataforma", null) + .WithMany() + .HasForeignKey("PlanoAlvoId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_pagamentos_treinador_planos_plataforma_plano_alvo_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_pagamentos_treinador_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.RefreshToken", b => + { + b.HasOne("forzion.tech.Domain.Entities.RefreshTokenFamily", null) + .WithMany() + .HasForeignKey("FamiliaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_refresh_token_families_familia_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.SerieConfig", b => + { + b.HasOne("forzion.tech.Domain.Entities.TreinoExercicio", null) + .WithMany("Series") + .HasForeignKey("TreinoExercicioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_treino_exercicio_series_treino_exercicios_treino_exercicio_"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.SystemUser", b => + { + b.HasOne("forzion.tech.Domain.Entities.Conta", null) + .WithMany() + .HasForeignKey("ContaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_system_users_contas_conta_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Treinador", b => + { + b.HasOne("forzion.tech.Domain.Entities.Conta", null) + .WithMany() + .HasForeignKey("ContaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_treinadores_contas_conta_id"); + + b.HasOne("forzion.tech.Domain.Entities.PlanoPlataforma", null) + .WithMany() + .HasForeignKey("PlanoPlataformaId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_treinadores_planos_plataforma_plano_plataforma_id"); + + b.OwnsOne("forzion.tech.Domain.ValueObjects.DadosFiscais", "DadosFiscais", b1 => + { + b1.Property("TreinadorId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b1.Property("Documento") + .IsRequired() + .HasMaxLength(14) + .HasColumnType("character varying(14)") + .HasColumnName("dados_fiscais_documento"); + + b1.Property("InscricaoMunicipal") + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("dados_fiscais_inscricao_municipal"); + + b1.Property("RazaoSocial") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("dados_fiscais_razao_social"); + + b1.Property("TipoDocumento") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("dados_fiscais_tipo_documento"); + + b1.HasKey("TreinadorId"); + + b1.ToTable("treinadores"); + + b1.WithOwner() + .HasForeignKey("TreinadorId") + .HasConstraintName("fk_treinadores_treinadores_id"); + + b1.OwnsOne("forzion.tech.Domain.ValueObjects.EnderecoFiscal", "Endereco", b2 => + { + b2.Property("DadosFiscaisTreinadorId") + .HasColumnType("uuid") + .HasColumnName("id"); + + b2.Property("Bairro") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dados_fiscais_endereco_bairro"); + + b2.Property("Cep") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("dados_fiscais_endereco_cep"); + + b2.Property("CodigoMunicipioIbge") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character varying(7)") + .HasColumnName("dados_fiscais_endereco_codigo_municipio_ibge"); + + b2.Property("Complemento") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dados_fiscais_endereco_complemento"); + + b2.Property("Logradouro") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("dados_fiscais_endereco_logradouro"); + + b2.Property("Numero") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("dados_fiscais_endereco_numero"); + + b2.Property("Uf") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)") + .HasColumnName("dados_fiscais_endereco_uf"); + + b2.HasKey("DadosFiscaisTreinadorId"); + + b2.ToTable("treinadores"); + + b2.WithOwner() + .HasForeignKey("DadosFiscaisTreinadorId") + .HasConstraintName("fk_treinadores_treinadores_id"); + }); + + b1.Navigation("Endereco") + .IsRequired(); + }); + + b.Navigation("DadosFiscais"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Treino", b => + { + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_treinos_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TreinoAluno", b => + { + b.HasOne("forzion.tech.Domain.Entities.Aluno", null) + .WithMany() + .HasForeignKey("AlunoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_treino_alunos_alunos_aluno_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treino", null) + .WithMany() + .HasForeignKey("TreinoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_treino_alunos_treinos_treino_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TreinoExercicio", b => + { + b.HasOne("forzion.tech.Domain.Entities.Exercicio", null) + .WithMany() + .HasForeignKey("ExercicioId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_treino_exercicios_exercicios_exercicio_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treino", null) + .WithMany("Exercicios") + .HasForeignKey("TreinoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_treino_exercicios_treinos_treino_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.VinculoTreinadorAluno", b => + { + b.HasOne("forzion.tech.Domain.Entities.Aluno", null) + .WithMany() + .HasForeignKey("AlunoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_vinculos_treinador_aluno_alunos_aluno_id"); + + b.HasOne("forzion.tech.Domain.Entities.Pacote", null) + .WithMany() + .HasForeignKey("PacoteId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vinculos_treinador_aluno_pacotes_pacote_id"); + + b.HasOne("forzion.tech.Domain.Entities.Treinador", null) + .WithMany() + .HasForeignKey("TreinadorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_vinculos_treinador_aluno_treinadores_treinador_id"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.ExecucaoTreino", b => + { + b.Navigation("Exercicios"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.Treino", b => + { + b.Navigation("Exercicios"); + }); + + modelBuilder.Entity("forzion.tech.Domain.Entities.TreinoExercicio", b => + { + b.Navigation("Series"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.cs b/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.cs new file mode 100644 index 00000000..97f1525f --- /dev/null +++ b/forzion.tech.Infrastructure/Migrations/20260626152551_CriarReconciliacaoStripeEstado.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace forzion.tech.Infrastructure.Migrations +{ + /// + public partial class CriarReconciliacaoStripeEstado : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "reconciliacao_stripe_estado", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + ultimo_evento_reconciliado_utc = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_reconciliacao_stripe_estado", x => x.id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "reconciliacao_stripe_estado"); + } + } +} diff --git a/forzion.tech.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/forzion.tech.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index e83af779..f0d7a00f 100644 --- a/forzion.tech.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/forzion.tech.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -1588,6 +1588,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("forzion.tech.Domain.Entities.ReconciliacaoStripeEstado", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UltimoEventoReconciliadoUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("ultimo_evento_reconciliado_utc"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_reconciliacao_stripe_estado"); + + b.ToTable("reconciliacao_stripe_estado", (string)null); + }); + modelBuilder.Entity("forzion.tech.Domain.Entities.RefreshToken", b => { b.Property("Id") diff --git a/forzion.tech.Infrastructure/Persistence/AppDbContext.cs b/forzion.tech.Infrastructure/Persistence/AppDbContext.cs index 62ad53a2..e3e52628 100644 --- a/forzion.tech.Infrastructure/Persistence/AppDbContext.cs +++ b/forzion.tech.Infrastructure/Persistence/AppDbContext.cs @@ -57,6 +57,7 @@ public class AppDbContext( public DbSet MfaChallenges => Set(); public DbSet TrustedDevices => Set(); public DbSet TrocaEmailTokens => Set(); + public DbSet ReconciliacoesStripeEstado => Set(); private EfCoreTransactionAdapter? _transacaoAtiva; private List? _eventosBestEffortPosCommit; diff --git a/forzion.tech.Infrastructure/Persistence/Configurations/ReconciliacaoStripeEstadoConfiguration.cs b/forzion.tech.Infrastructure/Persistence/Configurations/ReconciliacaoStripeEstadoConfiguration.cs new file mode 100644 index 00000000..423e3e3d --- /dev/null +++ b/forzion.tech.Infrastructure/Persistence/Configurations/ReconciliacaoStripeEstadoConfiguration.cs @@ -0,0 +1,18 @@ +using forzion.tech.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace forzion.tech.Infrastructure.Persistence.Configurations; + +public class ReconciliacaoStripeEstadoConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("reconciliacao_stripe_estado"); + builder.HasKey(e => e.Id); + + builder.Property(e => e.UltimoEventoReconciliadoUtc).IsRequired(); + builder.Property(e => e.CreatedAt).IsRequired(); + builder.Property(e => e.UpdatedAt); + } +} diff --git a/forzion.tech.Infrastructure/Persistence/Repositories/ContaRecebimentoRepository.cs b/forzion.tech.Infrastructure/Persistence/Repositories/ContaRecebimentoRepository.cs index bdcd543c..38fe00f0 100644 --- a/forzion.tech.Infrastructure/Persistence/Repositories/ContaRecebimentoRepository.cs +++ b/forzion.tech.Infrastructure/Persistence/Repositories/ContaRecebimentoRepository.cs @@ -18,6 +18,14 @@ await _context.ContasRecebimento .FirstOrDefaultAsync(c => c.StripeConnectAccountId == stripeAccountId, cancellationToken) .ConfigureAwait(false); + public async Task> ListarConfiguradasPendentesOnboardingAsync(int max, CancellationToken cancellationToken = default) => + await _context.ContasRecebimento + .Where(c => c.StripeConnectAccountId != null && !c.OnboardingCompleto) + .OrderBy(c => c.CreatedAt) + .Take(max) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + public async Task AdicionarAsync(ContaRecebimento conta, CancellationToken cancellationToken = default) => await _context.ContasRecebimento.AddAsync(conta, cancellationToken).ConfigureAwait(false); } diff --git a/forzion.tech.Infrastructure/Persistence/Repositories/ReconciliacaoStripeEstadoRepository.cs b/forzion.tech.Infrastructure/Persistence/Repositories/ReconciliacaoStripeEstadoRepository.cs new file mode 100644 index 00000000..82d6b8e9 --- /dev/null +++ b/forzion.tech.Infrastructure/Persistence/Repositories/ReconciliacaoStripeEstadoRepository.cs @@ -0,0 +1,19 @@ +using forzion.tech.Application.Interfaces.Repositories; +using forzion.tech.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace forzion.tech.Infrastructure.Persistence.Repositories; + +public class ReconciliacaoStripeEstadoRepository(AppDbContext context) : IReconciliacaoStripeEstadoRepository +{ + public async Task ObterAsync(CancellationToken cancellationToken = default) => + await context.ReconciliacoesStripeEstado + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + public async Task SalvarAsync(ReconciliacaoStripeEstado estado, CancellationToken cancellationToken = default) + { + if (context.Entry(estado).State == EntityState.Detached) + await context.ReconciliacoesStripeEstado.AddAsync(estado, cancellationToken).ConfigureAwait(false); + } +} diff --git a/forzion.tech.Infrastructure/Services/StripeService.cs b/forzion.tech.Infrastructure/Services/StripeService.cs index 1af4b6c6..2e9c9afb 100644 --- a/forzion.tech.Infrastructure/Services/StripeService.cs +++ b/forzion.tech.Infrastructure/Services/StripeService.cs @@ -82,6 +82,7 @@ public async Task CriarPixPaymentIntentAsync( string idempotencyKey, CancellationToken cancellationToken = default) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(valor); var (valorCentavos, taxaCentavos) = MoneyCentavos.ValorETaxaCentavos(valor, taxaPlataformaPercent); var service = new PaymentIntentService(); @@ -123,6 +124,7 @@ public async Task CriarCartaoPaymentIntentAsync( string idempotencyKey, CancellationToken cancellationToken = default) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(valor); var (valorCentavos, taxaCentavos) = MoneyCentavos.ValorETaxaCentavos(valor, taxaPlataformaPercent); var service = new PaymentIntentService(); @@ -150,6 +152,7 @@ public async Task CriarPixPlataformaPaymentIntentAsync( string idempotencyKey, CancellationToken cancellationToken = default) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(valor); var (valorCentavos, _) = MoneyCentavos.ValorETaxaCentavos(valor, 0m); var service = new PaymentIntentService(); @@ -188,6 +191,7 @@ public async Task CriarCartaoPlataformaPaymentIntentAsync( string idempotencyKey, CancellationToken cancellationToken = default) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(valor); var (valorCentavos, _) = MoneyCentavos.ValorETaxaCentavos(valor, 0m); var service = new PaymentIntentService(); @@ -325,11 +329,6 @@ private static string MontarTextoEvidencia(DisputaEvidencia e) } } - // Teto duro: 1000 eventos por execução. Janela default = 7d, com volume normal de webhooks - // pagáveis por treinador isso é folgado; passa de 1000 e algo está muito errado (ou janela - // foi pedida grande demais) — preferimos truncar e logar a perder controle. - private const int MaxEventosPorExecucao = 1000; - private static readonly string[] TiposReconciliaveis = [ "payment_intent.succeeded", @@ -340,14 +339,14 @@ private static string MontarTextoEvidencia(DisputaEvidencia e) "charge.dispute.created", ]; - public async Task> ListarEventosDesdeAsync( + public async Task ListarEventosDesdeAsync( DateTime desdeUtc, CancellationToken cancellationToken = default) { + var maxPorRun = _settings.MaxEventosReconciliacaoPorRun; var service = new EventService(); var options = new EventListOptions { - // Created.GreaterThanOrEqual aceita DateTime UTC; Stripe.net converte pra unix internally. Created = new DateRangeOptions { GreaterThanOrEqual = DateTime.SpecifyKind(desdeUtc, DateTimeKind.Utc), @@ -357,9 +356,8 @@ public async Task> ListarEventosDesdeAsync( }; var coletados = new List(); + var truncado = false; - // ListAutoPagingAsync resolve cursor pagination transparentemente. - // Stripe.net default page size = 100; cap externo em MaxEventosPorExecucao. await foreach (var evt in service.ListAutoPagingAsync(options, RequestOptions, cancellationToken).ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); @@ -370,22 +368,23 @@ public async Task> ListarEventosDesdeAsync( PayloadRaw: evt.ToJson(), Created: DateTime.SpecifyKind(evt.Created, DateTimeKind.Utc))); - if (coletados.Count >= MaxEventosPorExecucao) + if (coletados.Count >= maxPorRun) { + truncado = true; logger.LogWarning( "Reconciliação Stripe atingiu teto de {Max} eventos (desde {Desde:o}). Truncando varredura.", - MaxEventosPorExecucao, desdeUtc); + maxPorRun, desdeUtc); break; } } - // Stripe retorna em ordem DESC; reordena ASC para replays seguirem cronologia natural. + // Stripe retorna DESC; reordena ASC para o cursor avançar em cronologia natural. coletados.Sort((a, b) => a.Created.CompareTo(b.Created)); logger.LogInformation( - "Stripe Events.List retornou {Count} eventos relevantes desde {Desde:o}.", - coletados.Count, desdeUtc); + "Stripe Events.List retornou {Count} eventos relevantes desde {Desde:o} (truncado={Truncado}).", + coletados.Count, desdeUtc, truncado); - return coletados; + return new StripeEventListResult(coletados, truncado); } } diff --git a/forzion.tech.Infrastructure/Services/StripeSettings.cs b/forzion.tech.Infrastructure/Services/StripeSettings.cs index 0aeafc55..21299c0d 100644 --- a/forzion.tech.Infrastructure/Services/StripeSettings.cs +++ b/forzion.tech.Infrastructure/Services/StripeSettings.cs @@ -14,4 +14,5 @@ public class StripeSettings public int TimeoutSegundos { get; set; } = 30; public int MaxNetworkRetries { get; set; } = 2; + public int MaxEventosReconciliacaoPorRun { get; set; } = 1000; } diff --git a/forzion.tech.Infrastructure/forzion.tech.Infrastructure.csproj b/forzion.tech.Infrastructure/forzion.tech.Infrastructure.csproj index fab29ea0..2f608ee6 100644 --- a/forzion.tech.Infrastructure/forzion.tech.Infrastructure.csproj +++ b/forzion.tech.Infrastructure/forzion.tech.Infrastructure.csproj @@ -25,12 +25,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/forzion.tech.Tests/Api/Endpoints/TreinadorEndpointsTests.cs b/forzion.tech.Tests/Api/Endpoints/TreinadorEndpointsTests.cs index 4481cf65..63e20777 100644 --- a/forzion.tech.Tests/Api/Endpoints/TreinadorEndpointsTests.cs +++ b/forzion.tech.Tests/Api/Endpoints/TreinadorEndpointsTests.cs @@ -52,6 +52,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -190,6 +191,73 @@ public async Task Post_AprovarVinculo_NaoEncontrado_Retorna404() response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + [Fact] + public async Task Post_AprovarVinculo_TreinadorInadimplente_Retorna403() + { + _factory.AprovarVinculoHandlerMock + .Setup(h => h.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(RespostaVinculo)); + + var inadimplente = CriarAssinaturaTreinador(AssinaturaTreinadorStatus.Inadimplente); + var factory = _factory.WithWebHostBuilder(builder => + builder.ConfigureTestServices(services => + { + var repo = new Mock(); + repo.Setup(r => r.ObterAtualPorTreinadorAsync(TreinadorId, It.IsAny())) + .ReturnsAsync(inadimplente); + services.RemoveAll(); + services.AddScoped(_ => repo.Object); + })); + + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test", "treinador"); + + var response = await client.PostAsJsonAsync( + $"/treinador/vinculos/{VinculoId}/aprovar", + new { PacoteId = PacoteId, TrarFichas = false }); + + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Post_AprovarVinculo_TreinadorAtivo_Retorna200() + { + _factory.AprovarVinculoHandlerMock + .Setup(h => h.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(RespostaVinculo)); + + var ativa = CriarAssinaturaTreinador(AssinaturaTreinadorStatus.Ativa); + var factory = _factory.WithWebHostBuilder(builder => + builder.ConfigureTestServices(services => + { + var repo = new Mock(); + repo.Setup(r => r.ObterAtualPorTreinadorAsync(TreinadorId, It.IsAny())) + .ReturnsAsync(ativa); + services.RemoveAll(); + services.AddScoped(_ => repo.Object); + })); + + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test", "treinador"); + + var response = await client.PostAsJsonAsync( + $"/treinador/vinculos/{VinculoId}/aprovar", + new { PacoteId = PacoteId, TrarFichas = false }); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + private static forzion.tech.Domain.Entities.AssinaturaTreinador CriarAssinaturaTreinador(AssinaturaTreinadorStatus status) + { + var agora = DateTime.UtcNow; + var a = forzion.tech.Domain.Entities.AssinaturaTreinador.Criar(TreinadorId, Guid.NewGuid(), 100m, agora).Value; + if (status is AssinaturaTreinadorStatus.Ativa or AssinaturaTreinadorStatus.Inadimplente) + a.Ativar(agora); + if (status == AssinaturaTreinadorStatus.Inadimplente) + a.MarcarInadimplente(agora); + return a; + } + // --- POST /treinador/vinculos/{id}/desvincular --- [Fact] @@ -982,6 +1050,8 @@ public async Task Get_Danfse_NaoDono_Retorna404() public class TreinadorWebFactory : WebApplicationFactory { + public Mock AssinaturaTreinadorRepositoryMock { get; } = new(); + public Mock ListarAlunosHandlerMock { get; } = new( Mock.Of(), Mock.Of(), @@ -1221,6 +1291,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); + services.RemoveAll(); services.AddScoped(_ => ListarAlunosHandlerMock.Object); services.AddScoped(_ => AprovarVinculoHandlerMock.Object); @@ -1259,6 +1330,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) userContextMock.Setup(u => u.TipoConta).Returns(TipoConta.Treinador); services.AddScoped(_ => userContextMock.Object); + services.AddScoped(_ => AssinaturaTreinadorRepositoryMock.Object); + var jwtMock = new Mock(); jwtMock.Setup(j => j.ValidarTokenEscopo("step-up-ok", MfaScopes.StepUp)) .Returns(new EscopoValidado(ContaId, Guid.NewGuid())); diff --git a/forzion.tech.Tests/Api/Endpoints/TreinoEndpointsTests.cs b/forzion.tech.Tests/Api/Endpoints/TreinoEndpointsTests.cs index 8f21bd9d..2b8f5225 100644 --- a/forzion.tech.Tests/Api/Endpoints/TreinoEndpointsTests.cs +++ b/forzion.tech.Tests/Api/Endpoints/TreinoEndpointsTests.cs @@ -472,8 +472,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.RemoveAll(); var userContextMock = new Mock(); userContextMock.Setup(u => u.PerfilId).Returns(TreinadorId); + userContextMock.Setup(u => u.TipoConta).Returns(TipoConta.Treinador); services.AddScoped(_ => userContextMock.Object); + services.RemoveAll(); + var assinaturaTreinadorRepoMock = new Mock(); + services.AddScoped(_ => assinaturaTreinadorRepoMock.Object); + services.AddAuthentication("Test") .AddScheme("Test", _ => { }); }); diff --git a/forzion.tech.Tests/Api/Filters/RequireAssinaturaTreinadorAtivaFilterTests.cs b/forzion.tech.Tests/Api/Filters/RequireAssinaturaTreinadorAtivaFilterTests.cs index 91564947..12756ff4 100644 --- a/forzion.tech.Tests/Api/Filters/RequireAssinaturaTreinadorAtivaFilterTests.cs +++ b/forzion.tech.Tests/Api/Filters/RequireAssinaturaTreinadorAtivaFilterTests.cs @@ -1,8 +1,11 @@ using FluentAssertions; using forzion.tech.Api.Filters; using forzion.tech.Application.Interfaces; +using forzion.tech.Application.Interfaces.Repositories; +using forzion.tech.Domain.Entities; using forzion.tech.Domain.Enums; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -11,14 +14,25 @@ namespace forzion.tech.Tests.Api.Filters; public class RequireAssinaturaTreinadorAtivaFilterTests { private static (EndpointFilterInvocationContext ctx, RequireAssinaturaTreinadorAtivaFilter filter, EndpointFilterDelegate next, Action assertNextCalled) - Cenario(TipoConta tipoConta, string method = "POST") + Cenario( + TipoConta tipoConta, + string method = "POST", + AssinaturaTreinador? assinatura = null) { + var treinadorId = Guid.NewGuid(); + var userContext = new Mock(); userContext.SetupGet(u => u.TipoConta).Returns(tipoConta); - userContext.SetupGet(u => u.PerfilId).Returns(Guid.NewGuid()); + userContext.SetupGet(u => u.PerfilId).Returns(treinadorId); + + var assinaturaRepository = new Mock(); + assinaturaRepository + .Setup(r => r.ObterAtualPorTreinadorAsync(treinadorId, It.IsAny())) + .ReturnsAsync(assinatura); var services = new ServiceCollection(); services.AddSingleton(userContext.Object); + services.AddSingleton(assinaturaRepository.Object); var provider = services.BuildServiceProvider(); var httpContext = new DefaultHttpContext { RequestServices = provider }; @@ -35,6 +49,26 @@ private static (EndpointFilterInvocationContext ctx, RequireAssinaturaTreinadorA return (ctx, new RequireAssinaturaTreinadorAtivaFilter(), next, expected => called.Should().Be(expected)); } + private static AssinaturaTreinador CriarAssinatura(Guid treinadorId, AssinaturaTreinadorStatus status) + { + var agora = DateTime.UtcNow; + var a = AssinaturaTreinador.Criar(treinadorId, Guid.NewGuid(), 100m, agora).Value; + switch (status) + { + case AssinaturaTreinadorStatus.Ativa: + a.Ativar(agora); + break; + case AssinaturaTreinadorStatus.Inadimplente: + a.Ativar(agora); + a.MarcarInadimplente(agora); + break; + case AssinaturaTreinadorStatus.Cancelada: + a.Cancelar(agora); + break; + } + return a; + } + [Fact] public async Task Get_TreinadorInadimplente_PassaDireto() { @@ -46,9 +80,94 @@ public async Task Get_TreinadorInadimplente_PassaDireto() } [Fact] - public async Task Post_Treinador_ChamaNext() + public async Task Post_TreinadorInadimplente_Retorna403ComCode() + { + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Inadimplente); + + var userContext = new Mock(); + userContext.SetupGet(u => u.TipoConta).Returns(TipoConta.Treinador); + userContext.SetupGet(u => u.PerfilId).Returns(treinadorId); + + var assinaturaRepository = new Mock(); + assinaturaRepository + .Setup(r => r.ObterAtualPorTreinadorAsync(treinadorId, It.IsAny())) + .ReturnsAsync(assinatura); + + var services = new ServiceCollection(); + services.AddSingleton(userContext.Object); + services.AddSingleton(assinaturaRepository.Object); + var provider = services.BuildServiceProvider(); + + var httpContext = new DefaultHttpContext { RequestServices = provider }; + httpContext.Request.Method = "POST"; + var ctx = new DefaultEndpointFilterInvocationContext(httpContext); + + var called = false; + EndpointFilterDelegate next = _ => + { + called = true; + return ValueTask.FromResult(Results.Ok()); + }; + + var filter = new RequireAssinaturaTreinadorAtivaFilter(); + var result = await filter.InvokeAsync(ctx, next); + + called.Should().BeFalse(); + var problem = result.Should().BeOfType().Subject; + problem.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + problem.ProblemDetails.Title.Should().Be("Assinatura inadimplente"); + problem.ProblemDetails.Extensions.Should().ContainKey("code"); + problem.ProblemDetails.Extensions["code"].Should().Be("ASSINATURA_TREINADOR_INADIMPLENTE"); + } + + [Fact] + public async Task Put_TreinadorInadimplente_Retorna403() { - var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "POST"); + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Inadimplente); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "PUT", assinatura); + + var result = await filter.InvokeAsync(ctx, next); + + assertCalled(false); + result.Should().BeOfType() + .Which.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + } + + [Fact] + public async Task Delete_TreinadorInadimplente_Retorna403() + { + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Inadimplente); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "DELETE", assinatura); + + var result = await filter.InvokeAsync(ctx, next); + + assertCalled(false); + result.Should().BeOfType() + .Which.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + } + + [Fact] + public async Task Post_TreinadorComAssinaturaAtiva_ChamaNext() + { + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Ativa); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "POST", assinatura); + + var result = await filter.InvokeAsync(ctx, next); + + assertCalled(true); + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task Post_TreinadorComAssinaturaPendente_ChamaNext() + { + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Pendente); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "POST", assinatura); await filter.InvokeAsync(ctx, next); @@ -56,9 +175,11 @@ public async Task Post_Treinador_ChamaNext() } [Fact] - public async Task Put_Treinador_ChamaNext() + public async Task Post_TreinadorComAssinaturaCancelada_ChamaNext() { - var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "PUT"); + var treinadorId = Guid.NewGuid(); + var assinatura = CriarAssinatura(treinadorId, AssinaturaTreinadorStatus.Cancelada); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "POST", assinatura); await filter.InvokeAsync(ctx, next); @@ -66,9 +187,9 @@ public async Task Put_Treinador_ChamaNext() } [Fact] - public async Task Delete_Treinador_ChamaNext() + public async Task Post_TreinadorSemAssinatura_ChamaNext() { - var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "DELETE"); + var (ctx, filter, next, assertCalled) = Cenario(TipoConta.Treinador, "POST", assinatura: null); await filter.InvokeAsync(ctx, next); @@ -95,6 +216,38 @@ public async Task Post_SystemAdmin_Bypassa() assertCalled(true); } + [Fact] + public async Task Post_TipoNaoTreinador_NaoConsultaRepositorio() + { + var assinaturaRepoMock = new Mock(MockBehavior.Strict); + + var userContext = new Mock(); + userContext.SetupGet(u => u.TipoConta).Returns(TipoConta.Aluno); + userContext.SetupGet(u => u.PerfilId).Returns(Guid.NewGuid()); + + var services = new ServiceCollection(); + services.AddSingleton(userContext.Object); + services.AddSingleton(assinaturaRepoMock.Object); + var provider = services.BuildServiceProvider(); + + var httpContext = new DefaultHttpContext { RequestServices = provider }; + httpContext.Request.Method = "POST"; + var ctx = new DefaultEndpointFilterInvocationContext(httpContext); + + var called = false; + EndpointFilterDelegate next = _ => + { + called = true; + return ValueTask.FromResult(Results.Ok()); + }; + + var filter = new RequireAssinaturaTreinadorAtivaFilter(); + await filter.InvokeAsync(ctx, next); + + called.Should().BeTrue(); + assinaturaRepoMock.VerifyNoOtherCalls(); + } + [Fact] public async Task CodigoErro_EhAssinaturaTreinadorInadimplente() { diff --git a/forzion.tech.Tests/Application/Pagamentos/ProcessarWebhookStripeHandlerTests.cs b/forzion.tech.Tests/Application/Pagamentos/ProcessarWebhookStripeHandlerTests.cs index 62de10ae..0b9a9817 100644 --- a/forzion.tech.Tests/Application/Pagamentos/ProcessarWebhookStripeHandlerTests.cs +++ b/forzion.tech.Tests/Application/Pagamentos/ProcessarWebhookStripeHandlerTests.cs @@ -657,7 +657,7 @@ public async Task HandleAsync_ChargeRefunded_RefundTotal_MarcaEstornado() } [Fact] - public async Task HandleAsync_ChargeRefunded_AmountRefundedAusente_MarcaEstornado() + public async Task ProcessarChargeReembolsadoAsync_AmountRefundedNulo_Lanca() { const string payloadSemAmount = "{\"type\":\"charge.refunded\",\"data\":{\"object\":{\"id\":\"ch_x\",\"payment_intent\":\"pi_no_amount\"}}}"; @@ -670,11 +670,11 @@ public async Task HandleAsync_ChargeRefunded_AmountRefundedAusente_MarcaEstornad _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_no_amount", It.IsAny())) .ReturnsAsync(pagamento); - var result = await _handler.HandleAsync(new ProcessarWebhookStripeCommand(payloadSemAmount, ValidSig)); + var act = async () => await _handler.HandleAsync(new ProcessarWebhookStripeCommand(payloadSemAmount, ValidSig)); - result.IsSuccess.Should().BeTrue(); - pagamento.Status.Should().Be(PagamentoStatus.Estornado, "sem amount_refunded tratamos como total"); - _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Once); + await act.Should().ThrowAsync(); + pagamento.Status.Should().Be(PagamentoStatus.Pago); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); } private static string ChargeDisputeCreatedPayload(string paymentIntentId, string motivo = "fraudulent") => @@ -1255,6 +1255,9 @@ await _handler.HandleAsync(new ProcessarWebhookStripeCommand( _assinaturaRepo.Verify(r => r.ObterPorIdAsync(assinaturaId, It.IsAny()), Times.Once); } + private static string ChargeDisputeSemIdPayload(string paymentIntentId) => + "{\"type\":\"charge.dispute.created\",\"data\":{\"object\":{\"payment_intent\":\"" + paymentIntentId + "\",\"reason\":\"fraudulent\",\"amount\":14990}}}"; + private static string ChargeRefundedTreinadorPayload(string paymentIntentId, long amountCents = 5000) => "{\"type\":\"charge.refunded\",\"data\":{\"object\":{\"id\":\"ch_t\",\"payment_intent\":\"" + paymentIntentId + "\",\"amount_refunded\":" + amountCents + ",\"refunded\":true}}}"; @@ -1307,6 +1310,82 @@ public async Task HandleAsync_ChargeRefunded_PagamentoTreinadorJaEstornado_Idemp _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); } + [Fact] + public async Task ProcessarEstornoTreinadorAsync_AmountRefundedNulo_Lanca() + { + const string payloadSemAmount = + "{\"type\":\"charge.refunded\",\"data\":{\"object\":{\"id\":\"ch_t\",\"payment_intent\":\"pi_t_no_amount\",\"refunded\":true}}}"; + + var treinadorId = Guid.NewGuid(); + var assinatura = AssinaturaTreinador.Criar(treinadorId, Guid.NewGuid(), 50m, DateTime.UtcNow).Value; + assinatura.Ativar(DateTime.UtcNow); + var pagamento = PagamentoTreinador.Criar(treinadorId, assinatura.Id, 50m, FinalidadePagamentoTreinador.Renovacao, DateTime.UtcNow).Value; + pagamento.DefinirDadosPix("pi_t_no_amount", "qr", "url", DateTime.UtcNow.AddHours(1), DateTime.UtcNow); + pagamento.MarcarPago(DateTime.UtcNow); + pagamento.ClearDomainEvents(); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_t_no_amount", It.IsAny())).ReturnsAsync((Pagamento?)null); + _pagamentoTreinadorRepo.Setup(r => r.ObterPorStripePaymentIntentIdAsync("pi_t_no_amount", It.IsAny())).ReturnsAsync(pagamento); + + var act = async () => await _handler.HandleAsync(new ProcessarWebhookStripeCommand(payloadSemAmount, ValidSig)); + + await act.Should().ThrowAsync(); + pagamento.Status.Should().Be(PagamentoStatus.Pago); + _assinaturaTreinadorRepo.Verify(r => r.ObterPorIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ProcessarEstornoTreinadorAsync_RefundParcial_MantemPagoNaoMarcaInadimplente() + { + var treinadorId = Guid.NewGuid(); + var assinatura = AssinaturaTreinador.Criar(treinadorId, Guid.NewGuid(), 50m, DateTime.UtcNow).Value; + assinatura.Ativar(DateTime.UtcNow); + assinatura.ClearDomainEvents(); + var pagamento = PagamentoTreinador.Criar(treinadorId, assinatura.Id, 50m, FinalidadePagamentoTreinador.Renovacao, DateTime.UtcNow).Value; + pagamento.DefinirDadosPix("pi_t_partial_refund", "qr", "url", DateTime.UtcNow.AddHours(1), DateTime.UtcNow); + pagamento.MarcarPago(DateTime.UtcNow); + pagamento.ClearDomainEvents(); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_t_partial_refund", It.IsAny())).ReturnsAsync((Pagamento?)null); + _pagamentoTreinadorRepo.Setup(r => r.ObterPorStripePaymentIntentIdAsync("pi_t_partial_refund", It.IsAny())).ReturnsAsync(pagamento); + _assinaturaTreinadorRepo.Setup(r => r.ObterPorIdAsync(assinatura.Id, It.IsAny())).ReturnsAsync(assinatura); + + var result = await _handler.HandleAsync( + new ProcessarWebhookStripeCommand(ChargeRefundedTreinadorPayload("pi_t_partial_refund", 1000), ValidSig)); + + result.IsSuccess.Should().BeTrue(); + pagamento.Status.Should().Be(PagamentoStatus.Pago, "refund parcial não deve alterar status do PagamentoTreinador"); + assinatura.Status.Should().Be(AssinaturaTreinadorStatus.Ativa, "assinatura não deve ser marcada inadimplente por refund parcial"); + _assinaturaTreinadorRepo.Verify(r => r.ObterPorIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ProcessarEstornoTreinadorAsync_RefundTotal_MarcaEstornadoEInadimplenteEComita() + { + var treinadorId = Guid.NewGuid(); + var assinatura = AssinaturaTreinador.Criar(treinadorId, Guid.NewGuid(), 50m, DateTime.UtcNow).Value; + assinatura.Ativar(DateTime.UtcNow); + assinatura.ClearDomainEvents(); + var pagamento = PagamentoTreinador.Criar(treinadorId, assinatura.Id, 50m, FinalidadePagamentoTreinador.Renovacao, DateTime.UtcNow).Value; + pagamento.DefinirDadosPix("pi_t_full_refund", "qr", "url", DateTime.UtcNow.AddHours(1), DateTime.UtcNow); + pagamento.MarcarPago(DateTime.UtcNow); + pagamento.ClearDomainEvents(); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_t_full_refund", It.IsAny())).ReturnsAsync((Pagamento?)null); + _pagamentoTreinadorRepo.Setup(r => r.ObterPorStripePaymentIntentIdAsync("pi_t_full_refund", It.IsAny())).ReturnsAsync(pagamento); + _assinaturaTreinadorRepo.Setup(r => r.ObterPorIdAsync(assinatura.Id, It.IsAny())).ReturnsAsync(assinatura); + + var result = await _handler.HandleAsync( + new ProcessarWebhookStripeCommand(ChargeRefundedTreinadorPayload("pi_t_full_refund", 5000), ValidSig)); + + result.IsSuccess.Should().BeTrue(); + pagamento.Status.Should().Be(PagamentoStatus.Estornado); + assinatura.Status.Should().Be(AssinaturaTreinadorStatus.Inadimplente); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Once); + } + [Fact] public async Task HandleAsync_ChargeDisputeCreated_PagamentoTreinadorPago_MarcaEmDisputaECongela() { @@ -1464,4 +1543,62 @@ public async Task HandleAsync_PaymentIntentSucceeded_AssinaturaCancelada_RefundF await act.Should().ThrowAsync(); _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); } + + [Fact] + public async Task ProcessarDisputaCriadaAsync_DisputeIdVazio_PIMatchaPagamento_Lanca() + { + var pagamento = Pagamento.Criar(Guid.NewGuid(), 149.90m, DateTime.UtcNow).Value; + pagamento.DefinirDadosPix("pi_dis_noid", "qr", "url", DateTime.UtcNow.AddHours(1), TestData.Agora); + pagamento.MarcarPago(TestData.Agora); + pagamento.ClearDomainEvents(); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_dis_noid", It.IsAny())) + .ReturnsAsync(pagamento); + + var act = async () => await _handler.HandleAsync( + new ProcessarWebhookStripeCommand(ChargeDisputeSemIdPayload("pi_dis_noid"), ValidSig)); + + await act.Should().ThrowAsync(); + pagamento.Status.Should().Be(PagamentoStatus.Pago); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ProcessarDisputaTreinadorAsync_DisputeIdVazio_PIMatchaPagamentoTreinador_Lanca() + { + var treinadorId = Guid.NewGuid(); + var assinatura = AssinaturaTreinador.Criar(treinadorId, Guid.NewGuid(), 50m, DateTime.UtcNow).Value; + assinatura.Ativar(DateTime.UtcNow); + var pagamento = PagamentoTreinador.Criar(treinadorId, assinatura.Id, 50m, FinalidadePagamentoTreinador.Renovacao, DateTime.UtcNow).Value; + pagamento.DefinirDadosPix("pi_t_dis_noid", "qr", "url", DateTime.UtcNow.AddHours(1), DateTime.UtcNow); + pagamento.MarcarPago(DateTime.UtcNow); + pagamento.ClearDomainEvents(); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_t_dis_noid", It.IsAny())) + .ReturnsAsync((Pagamento?)null); + _pagamentoTreinadorRepo.Setup(r => r.ObterPorStripePaymentIntentIdAsync("pi_t_dis_noid", It.IsAny())) + .ReturnsAsync(pagamento); + + var act = async () => await _handler.HandleAsync( + new ProcessarWebhookStripeCommand(ChargeDisputeSemIdPayload("pi_t_dis_noid"), ValidSig)); + + await act.Should().ThrowAsync(); + pagamento.Status.Should().Be(PagamentoStatus.Pago); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ProcessarDisputaCriadaAsync_DisputeIdVazio_PIMatchaNada_NaoLanca() + { + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync("pi_ghost_noid", It.IsAny())) + .ReturnsAsync((Pagamento?)null); + _pagamentoTreinadorRepo.Setup(r => r.ObterPorStripePaymentIntentIdAsync("pi_ghost_noid", It.IsAny())) + .ReturnsAsync((PagamentoTreinador?)null); + + var result = await _handler.HandleAsync( + new ProcessarWebhookStripeCommand(ChargeDisputeSemIdPayload("pi_ghost_noid"), ValidSig)); + + result.IsSuccess.Should().BeTrue(); + _unitOfWork.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } } diff --git a/forzion.tech.Tests/Application/Pagamentos/ReconciliarPagamentosStripeHandlerTests.cs b/forzion.tech.Tests/Application/Pagamentos/ReconciliarPagamentosStripeHandlerTests.cs index e814cd5a..229cec04 100644 --- a/forzion.tech.Tests/Application/Pagamentos/ReconciliarPagamentosStripeHandlerTests.cs +++ b/forzion.tech.Tests/Application/Pagamentos/ReconciliarPagamentosStripeHandlerTests.cs @@ -20,6 +20,8 @@ public class ReconciliarPagamentosStripeHandlerTests private readonly Mock _contaRecebimentoRepo = new(); private readonly Mock _pagamentoTreinadorRepo = new(); private readonly Mock _unitOfWork = new(); + private readonly Mock _cursorRepo = new(); + private readonly Mock _cursorUow = new(); private readonly Mock> _webhookLogger = new(); private readonly Mock> _reconciliarLogger = new(); private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 5, 28, 12, 0, 0, TimeSpan.Zero)); @@ -40,9 +42,15 @@ public ReconciliarPagamentosStripeHandlerTests() var scopeFactory = new ServiceCollection() .AddSingleton(_webhookHandler) + .AddSingleton(_cursorRepo.Object) + .AddSingleton(_cursorUow.Object) + .AddSingleton(_contaRecebimentoRepo.Object) .BuildServiceProvider() .GetRequiredService(); + _contaRecebimentoRepo.Setup(r => r.ListarConfiguradasPendentesOnboardingAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + _handler = new ReconciliarPagamentosStripeHandler( _stripeService.Object, scopeFactory, _time, _reconciliarLogger.Object); } @@ -55,11 +63,13 @@ private static StripeEventSummary PaymentIntentEvento(string type, string paymen return new StripeEventSummary("evt_" + paymentIntentId, type, payload, created); } - private void SetupEventos(params StripeEventSummary[] eventos) + private void SetupEventos(params StripeEventSummary[] eventos) => SetupLote(false, eventos); + + private void SetupLote(bool truncado, params StripeEventSummary[] eventos) { _stripeService .Setup(s => s.ListarEventosDesdeAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(eventos); + .ReturnsAsync(new StripeEventListResult(eventos, truncado)); } [Fact] @@ -262,6 +272,141 @@ public async Task HandleAsync_VariosEventos_ProcessaTodosEAgrega() result.Value.Erros.Should().Be(0); } + [Fact] + public async Task HandleAsync_ComCursorDentroDaJanela_UsaCursorComoDesde() + { + var agora = _time.GetUtcNow().UtcDateTime; + var cursor = agora.AddDays(-2); + _cursorRepo.Setup(r => r.ObterAsync(It.IsAny())) + .ReturnsAsync(ReconciliacaoStripeEstado.Criar(cursor, agora)); + SetupEventos(); + + var result = await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + result.Value.DesdeUtc.Should().BeCloseTo(cursor, TimeSpan.FromSeconds(1)); + _stripeService.Verify(s => s.ListarEventosDesdeAsync( + It.Is(d => Math.Abs((d - cursor).TotalSeconds) < 1), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_CursorAnteriorAJanelaMax_CapeiaEmSeteDias() + { + var agora = _time.GetUtcNow().UtcDateTime; + var cursorAntigo = agora.AddDays(-30); + var janelaMax = agora.AddDays(-7); + _cursorRepo.Setup(r => r.ObterAsync(It.IsAny())) + .ReturnsAsync(ReconciliacaoStripeEstado.Criar(cursorAntigo, agora)); + SetupEventos(); + + var result = await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + result.Value.DesdeUtc.Should().BeCloseTo(janelaMax, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task HandleAsync_DesdeUtcExplicito_IgnoraCursor() + { + var agora = _time.GetUtcNow().UtcDateTime; + var desde = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc); + _cursorRepo.Setup(r => r.ObterAsync(It.IsAny())) + .ReturnsAsync(ReconciliacaoStripeEstado.Criar(agora.AddDays(-1), agora)); + SetupEventos(); + + await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand(desde)); + + _stripeService.Verify(s => s.ListarEventosDesdeAsync(desde, It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_AposProcessar_PersisteCursorComUltimoCreated() + { + ReconciliacaoStripeEstado? salvo = null; + _cursorRepo.Setup(r => r.SalvarAsync(It.IsAny(), It.IsAny())) + .Callback((e, _) => salvo = e) + .Returns(Task.CompletedTask); + + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Pagamento?)null); + var ultimoCreated = _time.GetUtcNow().UtcDateTime; + SetupEventos( + PaymentIntentEvento("payment_intent.succeeded", "pi_1", ultimoCreated.AddMinutes(-10)), + PaymentIntentEvento("payment_intent.succeeded", "pi_2", ultimoCreated)); + + await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + salvo.Should().NotBeNull(); + salvo!.UltimoEventoReconciliadoUtc.Should().BeCloseTo(ultimoCreated, TimeSpan.FromSeconds(1)); + _cursorUow.Verify(u => u.CommitAsync(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public async Task HandleAsync_ListaVazia_NaoPersisteCursor() + { + SetupEventos(); + + await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + _cursorRepo.Verify(r => r.SalvarAsync(It.IsAny(), It.IsAny()), Times.Never); + _cursorUow.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task HandleAsync_LoteTruncado_PropagaFlagETruncadoAvancaCursorAteUltimoProcessado() + { + ReconciliacaoStripeEstado? salvo = null; + _cursorRepo.Setup(r => r.SalvarAsync(It.IsAny(), It.IsAny())) + .Callback((e, _) => salvo = e) + .Returns(Task.CompletedTask); + _pagamentoRepo.Setup(r => r.ObterPorPaymentIntentIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Pagamento?)null); + var ultimoProcessado = _time.GetUtcNow().UtcDateTime; + SetupLote(true, PaymentIntentEvento("payment_intent.succeeded", "pi_trunc", ultimoProcessado)); + + var result = await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + result.Value.Truncado.Should().BeTrue(); + salvo!.UltimoEventoReconciliadoUtc.Should().BeCloseTo(ultimoProcessado, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task HandleAsync_ContaPendenteAtivadaNaStripe_ConfirmaOnboarding() + { + var agora = _time.GetUtcNow().UtcDateTime; + var conta = ContaRecebimento.Criar(Guid.NewGuid(), agora).Value; + conta.ConfigurarStripeConnect("acct_pend", agora); + SetupEventos(); + _contaRecebimentoRepo.Setup(r => r.ListarConfiguradasPendentesOnboardingAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new[] { conta }); + _contaRecebimentoRepo.Setup(r => r.ObterPorStripeAccountIdAsync("acct_pend", It.IsAny())) + .ReturnsAsync(conta); + _stripeService.Setup(s => s.ContaEstaAtivadaAsync("acct_pend", It.IsAny())) + .ReturnsAsync(true); + + var result = await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + result.Value.OnboardingConfirmados.Should().Be(1); + conta.OnboardingCompleto.Should().BeTrue(); + } + + [Fact] + public async Task HandleAsync_ContaPendenteAindaInativaNaStripe_NaoConfirma() + { + var agora = _time.GetUtcNow().UtcDateTime; + var conta = ContaRecebimento.Criar(Guid.NewGuid(), agora).Value; + conta.ConfigurarStripeConnect("acct_inativa", agora); + SetupEventos(); + _contaRecebimentoRepo.Setup(r => r.ListarConfiguradasPendentesOnboardingAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new[] { conta }); + _stripeService.Setup(s => s.ContaEstaAtivadaAsync("acct_inativa", It.IsAny())) + .ReturnsAsync(false); + + var result = await _handler.HandleAsync(new ReconciliarPagamentosStripeCommand()); + + result.Value.OnboardingConfirmados.Should().Be(0); + conta.OnboardingCompleto.Should().BeFalse(); + } + [Fact] public async Task HandleAsync_CommandNulo_LancaArgumentNullException() { diff --git a/forzion.tech.Tests/Application/Pagamentos/StripeWebhookParserTests.cs b/forzion.tech.Tests/Application/Pagamentos/StripeWebhookParserTests.cs index 76291d00..b53efc38 100644 --- a/forzion.tech.Tests/Application/Pagamentos/StripeWebhookParserTests.cs +++ b/forzion.tech.Tests/Application/Pagamentos/StripeWebhookParserTests.cs @@ -386,4 +386,73 @@ public void Parse_HappyPath_ChargeDisputeCreated_ValoresCorretos() result.PaymentIntentId.Should().Be("pi_disputed123"); result.MotivoDisputa.Should().Be("subscription_canceled"); } + + [Fact] + public void Parse_TypeComoNumero_NaoLanca() + { + var act = () => StripeWebhookParser.Parse("""{"type":123}"""); + act.Should().NotThrow(); + } + + [Fact] + public void Parse_TypeComoNumero_RetornaTipoVazio() + { + var result = StripeWebhookParser.Parse("""{"type":123}"""); + result.Type.Should().Be(string.Empty); + } + + [Fact] + public void Parse_TypeComoObjeto_NaoLanca() + { + var act = () => StripeWebhookParser.Parse("""{"type":{"nested":"value"}}"""); + act.Should().NotThrow(); + } + + [Fact] + public void Parse_TypeComoObjeto_RetornaTipoVazio() + { + var result = StripeWebhookParser.Parse("""{"type":{"nested":"value"}}"""); + result.Type.Should().Be(string.Empty); + } + + [Fact] + public void Parse_AccountUpdated_ChargesEnabledComoString_NaoLanca() + { + var act = () => StripeWebhookParser.Parse( + """{"type":"account.updated","data":{"object":{"charges_enabled":"true"}}}"""); + act.Should().NotThrow(); + } + + [Fact] + public void Parse_AccountUpdated_ChargesEnabledComoString_RetornaFalse() + { + var result = StripeWebhookParser.Parse( + """{"type":"account.updated","data":{"object":{"charges_enabled":"true"}}}"""); + result.ChargesEnabled.Should().BeFalse(); + } + + [Fact] + public void Parse_AccountUpdated_ChargesEnabledComoNumero_NaoLanca() + { + var act = () => StripeWebhookParser.Parse( + """{"type":"account.updated","data":{"object":{"charges_enabled":1}}}"""); + act.Should().NotThrow(); + } + + [Fact] + public void Parse_AccountUpdated_ChargesEnabledComoNumero_RetornaFalse() + { + var result = StripeWebhookParser.Parse( + """{"type":"account.updated","data":{"object":{"charges_enabled":1}}}"""); + result.ChargesEnabled.Should().BeFalse(); + } + + [Fact] + public void Parse_HappyPath_AccountUpdated_ChargesEnabledBool_RetornaTrue() + { + var result = StripeWebhookParser.Parse( + """{"type":"account.updated","account":"acct_ok","data":{"object":{"charges_enabled":true}}}"""); + result.ChargesEnabled.Should().BeTrue(); + result.AccountId.Should().Be("acct_ok"); + } } diff --git a/forzion.tech.Tests/Architecture/SpecInventoryTests.cs b/forzion.tech.Tests/Architecture/SpecInventoryTests.cs index 0e1debd0..55228c3b 100644 --- a/forzion.tech.Tests/Architecture/SpecInventoryTests.cs +++ b/forzion.tech.Tests/Architecture/SpecInventoryTests.cs @@ -14,8 +14,8 @@ namespace forzion.tech.Tests.Architecture; /// public class SpecInventoryTests { - private const int DbSetsEsperados = 40; - private const int RepositoriosEsperados = 38; + private const int DbSetsEsperados = 41; + private const int RepositoriosEsperados = 39; [Fact] public void AppDbContext_TemContagemDeDbSetsDocumentada() diff --git a/forzion.tech.Tests/E2E/FakeStripeService.cs b/forzion.tech.Tests/E2E/FakeStripeService.cs index 13640022..391b6a92 100644 --- a/forzion.tech.Tests/E2E/FakeStripeService.cs +++ b/forzion.tech.Tests/E2E/FakeStripeService.cs @@ -62,6 +62,6 @@ public Task CancelarPaymentIntentAsync(string pa public Task EnviarEvidenciaDisputaAsync(string disputeId, DisputaEvidencia evidencias, CancellationToken cancellationToken = default) => Task.CompletedTask; - public Task> ListarEventosDesdeAsync(DateTime desdeUtc, CancellationToken cancellationToken = default) => - Task.FromResult>(Array.Empty()); + public Task ListarEventosDesdeAsync(DateTime desdeUtc, CancellationToken cancellationToken = default) => + Task.FromResult(new StripeEventListResult(Array.Empty(), false)); } diff --git a/forzion.tech.Tests/Infrastructure/DependencyInjection/DeliveryLogSettingsTests.cs b/forzion.tech.Tests/Infrastructure/DependencyInjection/DeliveryLogSettingsTests.cs index fef10621..0803f5f3 100644 --- a/forzion.tech.Tests/Infrastructure/DependencyInjection/DeliveryLogSettingsTests.cs +++ b/forzion.tech.Tests/Infrastructure/DependencyInjection/DeliveryLogSettingsTests.cs @@ -7,6 +7,7 @@ namespace forzion.tech.Tests.Infrastructure.DependencyInjection; +[Collection(EnvironmentSensitiveCollection.Name)] public class DeliveryLogSettingsTests { private static ServiceProvider BuildProvider(string? environment, Dictionary? config = null) diff --git a/forzion.tech.Tests/Infrastructure/DependencyInjection/EnvironmentSensitiveCollection.cs b/forzion.tech.Tests/Infrastructure/DependencyInjection/EnvironmentSensitiveCollection.cs new file mode 100644 index 00000000..315bb22f --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/DependencyInjection/EnvironmentSensitiveCollection.cs @@ -0,0 +1,7 @@ +namespace forzion.tech.Tests.Infrastructure.DependencyInjection; + +[CollectionDefinition(Name, DisableParallelization = true)] +public class EnvironmentSensitiveCollection +{ + public const string Name = "EnvironmentSensitive"; +} diff --git a/forzion.tech.Tests/Infrastructure/DependencyInjection/InternalSettingsTests.cs b/forzion.tech.Tests/Infrastructure/DependencyInjection/InternalSettingsTests.cs new file mode 100644 index 00000000..bc9da515 --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/DependencyInjection/InternalSettingsTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using forzion.tech.Application.Settings; +using forzion.tech.Infrastructure.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace forzion.tech.Tests.Infrastructure.DependencyInjection; + +[Collection(EnvironmentSensitiveCollection.Name)] +public class InternalSettingsTests +{ + private static ServiceProvider BuildProvider(string? environment, Dictionary? config = null) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(config ?? []).Build(); + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(); + + var original = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + try + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", environment); + services.AddInfrastructure(configuration); + } + finally + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", original); + } + + return services.BuildServiceProvider(); + } + + [Fact] + public void Production_SemChave_FalhaFechado() + { + using var sp = BuildProvider("Production"); + + var act = () => _ = sp.GetRequiredService>().Value; + + act.Should().Throw(); + } + + [Fact] + public void Production_ComChave_Sobe() + { + using var sp = BuildProvider("Production", + new Dictionary { ["Internal:ApiKey"] = "prod-internal-key" }); + + sp.GetRequiredService>().Value + .ApiKey.Should().Be("prod-internal-key"); + } + + [Fact] + public void NaoProducao_SemChave_Sobe() + { + using var sp = BuildProvider("Development"); + + sp.GetRequiredService>().Value + .ApiKey.Should().BeEmpty(); + } +} diff --git a/forzion.tech.Tests/Infrastructure/DependencyInjection/StripeSettingsTests.cs b/forzion.tech.Tests/Infrastructure/DependencyInjection/StripeSettingsTests.cs new file mode 100644 index 00000000..374c1f78 --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/DependencyInjection/StripeSettingsTests.cs @@ -0,0 +1,127 @@ +using FluentAssertions; +using forzion.tech.Infrastructure.DependencyInjection; +using forzion.tech.Infrastructure.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace forzion.tech.Tests.Infrastructure.DependencyInjection; + +[Collection(EnvironmentSensitiveCollection.Name)] +public class StripeSettingsTests +{ + private static readonly Dictionary ConfigBase = new() + { + ["Stripe:SecretKey"] = "sk_test_basekey", + ["Stripe:WebhookSecret"] = "whsec_base", + }; + + private static ServiceProvider BuildProvider(string? environment, Dictionary config) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(config).Build(); + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(); + + var original = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + try + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", environment); + services.AddInfrastructure(configuration); + } + finally + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", original); + } + + return services.BuildServiceProvider(); + } + + [Fact] + public void ChaveSkLive_ExpectLivemodeNaoConfigurado_EmDesenvolvimento_FalhaBoot() + { + var config = new Dictionary(ConfigBase) + { + ["Stripe:SecretKey"] = "sk_live_verysecretkey123", + }; + using var sp = BuildProvider("Development", config); + + var act = () => _ = sp.GetRequiredService>().Value; + + var ex = act.Should().Throw().Which; + ex.Failures.Should().NotBeEmpty(); + ex.Failures.Should().AllSatisfy(f => f.Should().NotContain("verysecretkey123")); + } + + [Fact] + public void ChaveSkTest_ExpectLivemodeTrueExplicito_FalhaBoot() + { + var config = new Dictionary(ConfigBase) + { + ["Stripe:SecretKey"] = "sk_test_verysecretkey456", + ["Stripe:ExpectLivemode"] = "true", + }; + using var sp = BuildProvider("Development", config); + + var act = () => _ = sp.GetRequiredService>().Value; + + var ex = act.Should().Throw().Which; + ex.Failures.Should().NotBeEmpty(); + ex.Failures.Should().AllSatisfy(f => f.Should().NotContain("verysecretkey456")); + } + + [Fact] + public void ChaveSkLive_ExpectLivemodeTrueExplicito_Sobe() + { + var config = new Dictionary(ConfigBase) + { + ["Stripe:SecretKey"] = "sk_live_validprodkey", + ["Stripe:ExpectLivemode"] = "true", + }; + using var sp = BuildProvider("Production", config); + + var settings = sp.GetRequiredService>().Value; + + settings.SecretKey.Should().StartWith("sk_live_"); + settings.ExpectLivemode.Should().BeTrue(); + } + + [Fact] + public void ChaveSkTest_ExpectLivemodeNaoConfigurado_EmDesenvolvimento_Sobe() + { + using var sp = BuildProvider("Development", new Dictionary(ConfigBase)); + + var settings = sp.GetRequiredService>().Value; + + settings.SecretKey.Should().StartWith("sk_test_"); + settings.ExpectLivemode.Should().BeFalse(); + } + + [Fact] + public void TaxaAcimaDe100_FalhaBoot() + { + var config = new Dictionary(ConfigBase) + { + ["Stripe:TaxaPlataformaPercent"] = "101", + }; + using var sp = BuildProvider("Development", config); + + var act = () => _ = sp.GetRequiredService>().Value; + + act.Should().Throw(); + } + + [Fact] + public void TaxaIgualA100_Sobe() + { + var config = new Dictionary(ConfigBase) + { + ["Stripe:TaxaPlataformaPercent"] = "100", + }; + using var sp = BuildProvider("Development", config); + + var settings = sp.GetRequiredService>().Value; + + settings.TaxaPlataformaPercent.Should().Be(100m); + } +} diff --git a/forzion.tech.Tests/Infrastructure/Repositories/ContaRecebimentoPendentesOnboardingQueryTests.cs b/forzion.tech.Tests/Infrastructure/Repositories/ContaRecebimentoPendentesOnboardingQueryTests.cs new file mode 100644 index 00000000..4c111e32 --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/Repositories/ContaRecebimentoPendentesOnboardingQueryTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using forzion.tech.Domain.Entities; +using forzion.tech.Domain.Enums; +using forzion.tech.Domain.ValueObjects; +using forzion.tech.Infrastructure.Persistence; +using forzion.tech.Infrastructure.Persistence.Repositories; + +namespace forzion.tech.Tests.Infrastructure.Repositories; + +[Collection(InfrastructureTestCollection.Name)] +[Trait("Category", "Integration")] +public class ContaRecebimentoPendentesOnboardingQueryTests(InfrastructureTestFixture fixture) +{ + private static async Task SeedContaAsync( + AppDbContext ctx, bool configurada, bool onboardingCompleto, DateTime criadaEm) + { + var conta = Conta.Criar(Email.Criar($"t{Guid.NewGuid():N}@test.com").Value, "hash", TipoConta.Treinador, criadaEm).Value; + var treinador = Treinador.Criar(conta.Id, $"Tr{Guid.NewGuid():N}", criadaEm).Value; + var contaRecebimento = ContaRecebimento.Criar(treinador.Id, criadaEm).Value; + if (configurada) + contaRecebimento.ConfigurarStripeConnect($"acct_{Guid.NewGuid():N}", criadaEm); + if (onboardingCompleto) + contaRecebimento.ConfirmarOnboarding(criadaEm); + + await ctx.Contas.AddAsync(conta); + await ctx.Treinadores.AddAsync(treinador); + await ctx.ContasRecebimento.AddAsync(contaRecebimento); + await ctx.SaveChangesAsync(); + return contaRecebimento; + } + + [Fact] + public async Task RetornaSomenteConfiguradasNaoCompletas() + { + await using var ctx = fixture.CreateContext(); + var agora = DateTime.UtcNow; + var pendente = await SeedContaAsync(ctx, configurada: true, onboardingCompleto: false, agora); + await SeedContaAsync(ctx, configurada: true, onboardingCompleto: true, agora); + await SeedContaAsync(ctx, configurada: false, onboardingCompleto: false, agora); + + var resultado = await new ContaRecebimentoRepository(ctx).ListarConfiguradasPendentesOnboardingAsync(100); + + resultado.Should().Contain(c => c.Id == pendente.Id); + resultado.Should().OnlyContain(c => c.Configurada && !c.OnboardingCompleto); + } + + [Fact] + public async Task RespeitaCapDeBatch() + { + await using var ctx = fixture.CreateContext(); + var agora = DateTime.UtcNow; + await SeedContaAsync(ctx, configurada: true, onboardingCompleto: false, agora); + await SeedContaAsync(ctx, configurada: true, onboardingCompleto: false, agora.AddSeconds(1)); + await SeedContaAsync(ctx, configurada: true, onboardingCompleto: false, agora.AddSeconds(2)); + + var resultado = await new ContaRecebimentoRepository(ctx).ListarConfiguradasPendentesOnboardingAsync(2); + + resultado.Should().HaveCount(2); + } +} diff --git a/forzion.tech.Tests/Infrastructure/Repositories/ReconciliacaoStripeEstadoRepositoryTests.cs b/forzion.tech.Tests/Infrastructure/Repositories/ReconciliacaoStripeEstadoRepositoryTests.cs new file mode 100644 index 00000000..5265f6fa --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/Repositories/ReconciliacaoStripeEstadoRepositoryTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using forzion.tech.Domain.Entities; +using forzion.tech.Infrastructure.Persistence.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace forzion.tech.Tests.Infrastructure.Repositories; + +[Collection(InfrastructureTestCollection.Name)] +[Trait("Category", "Integration")] +public class ReconciliacaoStripeEstadoRepositoryTests(InfrastructureTestFixture fixture) +{ + private async Task LimparAsync() + { + await using var ctx = fixture.CreateContext(); + await ctx.ReconciliacoesStripeEstado.ExecuteDeleteAsync(); + } + + [Fact] + public async Task ObterAsync_SemRegistro_RetornaNull() + { + await LimparAsync(); + + await using var ctx = fixture.CreateContext(); + var resultado = await new ReconciliacaoStripeEstadoRepository(ctx).ObterAsync(); + + resultado.Should().BeNull(); + } + + [Fact] + public async Task SalvarAsync_PersisteEObterAsyncRetorna() + { + await LimparAsync(); + var cursor = new DateTime(2026, 6, 20, 0, 0, 0, DateTimeKind.Utc); + var estado = ReconciliacaoStripeEstado.Criar(cursor, DateTime.UtcNow); + + await using (var ctx = fixture.CreateContext()) + { + await new ReconciliacaoStripeEstadoRepository(ctx).SalvarAsync(estado); + await ctx.SaveChangesAsync(); + } + + await using (var ctx = fixture.CreateContext()) + { + var resultado = await new ReconciliacaoStripeEstadoRepository(ctx).ObterAsync(); + resultado.Should().NotBeNull(); + resultado!.Id.Should().Be(estado.Id); + resultado.UltimoEventoReconciliadoUtc.Should().Be(cursor); + } + } + + [Fact] + public async Task AvancarCursor_ViaTracking_PersisteAlteracoes() + { + await LimparAsync(); + var cursor = new DateTime(2026, 6, 20, 0, 0, 0, DateTimeKind.Utc); + var estado = ReconciliacaoStripeEstado.Criar(cursor, DateTime.UtcNow); + + await using (var ctx = fixture.CreateContext()) + { + await new ReconciliacaoStripeEstadoRepository(ctx).SalvarAsync(estado); + await ctx.SaveChangesAsync(); + } + + var novoCursor = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc); + await using (var ctx = fixture.CreateContext()) + { + var repo = new ReconciliacaoStripeEstadoRepository(ctx); + var atual = await repo.ObterAsync(); + atual!.AvancarCursor(novoCursor, DateTime.UtcNow); + await repo.SalvarAsync(atual); + await ctx.SaveChangesAsync(); + } + + await using (var ctx = fixture.CreateContext()) + { + var resultado = await new ReconciliacaoStripeEstadoRepository(ctx).ObterAsync(); + resultado!.UltimoEventoReconciliadoUtc.Should().Be(novoCursor); + resultado.UpdatedAt.Should().NotBeNull(); + } + } + + [Fact] + public async Task AvancarCursor_RetrocessoIgnorado() + { + var cursor = new DateTime(2026, 6, 20, 0, 0, 0, DateTimeKind.Utc); + var estado = ReconciliacaoStripeEstado.Criar(cursor, DateTime.UtcNow); + + estado.AvancarCursor(new DateTime(2026, 6, 10, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow); + + estado.UltimoEventoReconciliadoUtc.Should().Be(cursor); + estado.UpdatedAt.Should().BeNull(); + } +} diff --git a/forzion.tech.Tests/Infrastructure/Services/StripeServicePaymentIntentGuardTests.cs b/forzion.tech.Tests/Infrastructure/Services/StripeServicePaymentIntentGuardTests.cs new file mode 100644 index 00000000..70131c6a --- /dev/null +++ b/forzion.tech.Tests/Infrastructure/Services/StripeServicePaymentIntentGuardTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using forzion.tech.Infrastructure.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace forzion.tech.Tests.Infrastructure.Services; + +public class StripeServicePaymentIntentGuardTests +{ + private static StripeService Criar() => + new(Options.Create(new StripeSettings { SecretKey = "sk_test_x" }), TimeProvider.System, NullLogger.Instance); + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task CriarPixPaymentIntentAsync_ValorZeroOuNegativo_Lanca(decimal valor) + { + var svc = Criar(); + + var act = () => svc.CriarPixPaymentIntentAsync(valor, "acct_x", 0.1m, "key"); + + await act.Should().ThrowAsync(); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task CriarCartaoPaymentIntentAsync_ValorZeroOuNegativo_Lanca(decimal valor) + { + var svc = Criar(); + + var act = () => svc.CriarCartaoPaymentIntentAsync(valor, "acct_x", 0.1m, "key"); + + await act.Should().ThrowAsync(); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task CriarPixPlataformaPaymentIntentAsync_ValorZeroOuNegativo_Lanca(decimal valor) + { + var svc = Criar(); + + var act = () => svc.CriarPixPlataformaPaymentIntentAsync(valor, "key"); + + await act.Should().ThrowAsync(); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task CriarCartaoPlataformaPaymentIntentAsync_ValorZeroOuNegativo_Lanca(decimal valor) + { + var svc = Criar(); + + var act = () => svc.CriarCartaoPlataformaPaymentIntentAsync(valor, "key"); + + await act.Should().ThrowAsync(); + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 77d2b490..1f0ea478 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,8 +14,8 @@ "@mui/icons-material": "^9.1.1", "@mui/material": "^9.1.2", "@mui/material-nextjs": "^9.1.1", - "@mui/x-date-pickers": "^9.6.0", - "@sentry/nextjs": "^10.60.0", + "@mui/x-date-pickers": "^9.7.0", + "@sentry/nextjs": "^10.61.0", "@stripe/react-stripe-js": "^6.6.0", "@stripe/stripe-js": "^9.8.0", "@tanstack/react-query": "^5.101.1", @@ -57,7 +57,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@types/seedrandom": "^3.0.8", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^6.0.3", "@vitest/coverage-v8": "^4.1.9", "axe-core": "^4.11.4", "cross-env": "^10.1.0", @@ -75,7 +75,7 @@ "knip": "^5.88.1", "license-checker": "^25.0.1", "linkinator": "^7.6.1", - "lint-staged": "^16.4.0", + "lint-staged": "^17.0.8", "madge": "^8.0.0", "msw": "^2.14.6", "msw-storybook-addon": "^2.0.7", @@ -85,6 +85,7 @@ "storybook": "^10.4.6", "tsx": "^4.22.4", "typescript": "^6.0.3", + "vite": "^8.1.0", "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.9", "vitest-axe": "^1.0.0-pre.5" @@ -1728,38 +1729,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", @@ -2246,14 +2215,14 @@ } }, "node_modules/@base-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.1.tgz", + "integrity": "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", - "reselect": "^5.1.1", + "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { @@ -3933,9 +3902,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3952,9 +3918,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3971,9 +3934,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3990,9 +3950,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -4009,9 +3966,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -4028,9 +3982,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -4047,9 +3998,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -4066,9 +4014,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -4085,9 +4030,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4110,9 +4052,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4135,9 +4074,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4160,9 +4096,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4185,9 +4118,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4210,9 +4140,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4235,9 +4162,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4260,9 +4184,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5909,36 +5830,6 @@ } } }, - "node_modules/@mui/material/node_modules/@mui/utils": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", - "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/types": "^9.1.1", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/private-theming": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.1.1.tgz", @@ -5966,36 +5857,6 @@ } } }, - "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", - "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/types": "^9.1.1", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/styled-engine": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.1.1.tgz", @@ -6070,36 +5931,6 @@ } } }, - "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", - "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/types": "^9.1.1", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/types": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.1.1.tgz", @@ -6118,17 +5949,17 @@ } }, "node_modules/@mui/utils": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.1.tgz", - "integrity": "sha512-f3UO3jNN1pYg5zxqXC81Bvv8hx5ACcYc0387382ZI7M5ono1heIwHYLrKsz85myguWdeVKPRZGmDdynWUBjK2g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", + "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@mui/types": "^9.0.0", + "@mui/types": "^9.1.1", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.2.4" + "react-is": "^19.2.6" }, "engines": { "node": ">=14.0.0" @@ -6148,14 +5979,14 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.6.0.tgz", - "integrity": "sha512-Cdk0qkdfxjQtDAnrQdrkCCGyXifjmqzzoDnnoPPxLz5915BWW33cAG9RcZFUAupRaUuh3i6QE8C5oUA9Mrx+1A==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.7.0.tgz", + "integrity": "sha512-gW/tz5LKhuwl5/naP/gv4jT3e3Fcf9gXEemvsWX6gVnO4IkO/GUJ55UbPQVooLCaJRAR/WBwMXgen7nIrBTBMw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", - "@mui/utils": "^9.0.1", - "@mui/x-internals": "^9.6.0", + "@mui/utils": "^9.1.1", + "@mui/x-internals": "^9.7.0", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -6214,14 +6045,14 @@ } }, "node_modules/@mui/x-internals": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.6.0.tgz", - "integrity": "sha512-lBh+4P2CRyspoFbBwCemTFIYmAAX8esznDsYYDGgV0+Id9z7Xi5pRZUAFY33XodYgTnMPiN4TJyfd8bfJbPNzg==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.7.0.tgz", + "integrity": "sha512-fdBwh96L78QFZXB1oS2v8y33gXap3A6Tc0+AJYxYzLsRwx05WnaWhtHEwNV2x7zLXLaZ4ux0CCAJIgZ6kTP4FA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", - "@base-ui/utils": "^0.2.9", - "@mui/utils": "^9.0.1", + "@base-ui/utils": "^0.3.0", + "@mui/utils": "^9.1.1", "core-js-pure": "^3.49.0", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" @@ -6238,14 +6069,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -6321,9 +6152,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6340,9 +6168,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6359,9 +6184,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6378,9 +6200,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6835,9 +6654,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6855,9 +6671,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6875,9 +6688,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6895,9 +6705,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6915,9 +6722,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6935,9 +6739,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6955,9 +6756,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6975,9 +6773,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7201,9 +6996,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7218,9 +7010,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7235,9 +7024,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7252,9 +7038,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7269,9 +7052,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7286,9 +7066,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7303,9 +7080,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7320,9 +7094,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7502,9 +7273,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7519,9 +7287,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7536,9 +7301,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7553,9 +7315,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8253,10 +8012,301 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -8405,9 +8455,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8421,9 +8468,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8437,9 +8481,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8453,9 +8494,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8469,9 +8507,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8485,9 +8520,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8501,9 +8533,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8517,9 +8546,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8533,9 +8559,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8549,9 +8572,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8565,9 +8585,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8581,9 +8598,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8597,9 +8611,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8723,46 +8734,46 @@ } }, "node_modules/@sentry/browser": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.60.0.tgz", - "integrity": "sha512-20vzPKGrmupJPCaWd+soCOLkZRwKZxt0AVF4XGPNoGp7D7wVPRlHf+FWwVMx+rRRkahKupFefM2D9YoiUiBeag==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.62.0.tgz", + "integrity": "sha512-uJi0yPssB3Nt/cZ8/S8opW42gaM59/6IyNtPFYD7C0ciudi/nIo5QMVpCYBBI3jnKFOIQLlsMT4pDlOLuxxNuQ==", "license": "MIT", "dependencies": { - "@sentry/browser-utils": "10.60.0", - "@sentry/core": "10.60.0", - "@sentry/feedback": "10.60.0", - "@sentry/replay": "10.60.0", - "@sentry/replay-canvas": "10.60.0" + "@sentry/browser-utils": "10.62.0", + "@sentry/core": "10.62.0", + "@sentry/feedback": "10.62.0", + "@sentry/replay": "10.62.0", + "@sentry/replay-canvas": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/browser-utils": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.60.0.tgz", - "integrity": "sha512-YhdPeMJnMaKVi5NQ2tD9RJ2AxU7cav5khmiMHFppmbP3I3Os2EHVaQjAVb6/ePAINr/d5mj5SxpnWmFX17iXsg==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.62.0.tgz", + "integrity": "sha512-mS9HVVuWIdye9o0xUGFmzNOBqktF4n5kugrF8NCOYYDrr5ZV8Cx7BlquHQn5UpCeViVhZtcDlEm4iOK7++Px7A==", "license": "MIT", "dependencies": { - "@sentry/core": "10.60.0" + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/browser-utils/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/browser/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" @@ -9059,21 +9070,21 @@ } }, "node_modules/@sentry/feedback": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.60.0.tgz", - "integrity": "sha512-RcGUgaI8yrIXunhNLpdNLsBUJIDvnEGDRmFhC5v0oMltoGtovrIqrEhPXEiSWQvNB0x4q33ejkLeJRJoSDOp2w==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.62.0.tgz", + "integrity": "sha512-d0BVjJVny6qpBgGJgWL0fbcoQHjtD3z3R8EK/KzTS3RO92JX5n3A536n5D/rh0gZFgcIwiUzBXegmyPOSQn9ng==", "license": "MIT", "dependencies": { - "@sentry/core": "10.60.0" + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/feedback/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" @@ -9096,21 +9107,21 @@ } }, "node_modules/@sentry/nextjs": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.60.0.tgz", - "integrity": "sha512-7QModZzPQ6UpNeLaqZfCm2v/nNaR+9za+YkQ/t5Hj6gOZTmac+UUFcDpzSMsL1/WyuIL4pVYLv4p2Ti4BqQsnQ==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.62.0.tgz", + "integrity": "sha512-JyxKNruI/yx8/YoxR/MG0IYg/zPBY9LSeUUbWPWN4jPFOD9e5/cQh4GtdY6tFWcWl2262PS7me8mbTH0J2jjzg==", "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", "@rollup/plugin-commonjs": "28.0.1", - "@sentry/browser-utils": "10.60.0", + "@sentry/browser-utils": "10.62.0", "@sentry/bundler-plugin-core": "^5.3.0", "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.60.0", - "@sentry/node": "10.60.0", - "@sentry/opentelemetry": "10.60.0", - "@sentry/react": "10.60.0", - "@sentry/vercel-edge": "10.60.0", + "@sentry/core": "10.62.0", + "@sentry/node": "10.62.0", + "@sentry/opentelemetry": "10.62.0", + "@sentry/react": "10.62.0", + "@sentry/vercel-edge": "10.62.0", "@sentry/webpack-plugin": "^5.3.0", "rollup": "^4.60.3", "stacktrace-parser": "^0.1.11" @@ -9123,28 +9134,28 @@ } }, "node_modules/@sentry/nextjs/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/nextjs/node_modules/@sentry/node": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.60.0.tgz", - "integrity": "sha512-u//paUrkKaCr0oNn7r7UulGydkYMSkU1wQOIpG/P/jf7psZWnyXhgeszHzUfZXo6pCdxXG9z9viPvzGjqPQN7A==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.62.0.tgz", + "integrity": "sha512-4hoU67bJY0o3irEDMZu2UIztAOsvEqFkLXA7EUKl1LXMA3Ba1Lb32OUVqlsTypiEInSDs/BtM+aAFKojZ3P3Fw==", "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", - "@sentry/core": "10.60.0", - "@sentry/node-core": "10.60.0", - "@sentry/opentelemetry": "10.60.0", - "@sentry/server-utils": "10.60.0", + "@sentry/core": "10.62.0", + "@sentry/node-core": "10.62.0", + "@sentry/opentelemetry": "10.62.0", + "@sentry/server-utils": "10.62.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -9169,14 +9180,14 @@ } }, "node_modules/@sentry/node-core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.60.0.tgz", - "integrity": "sha512-aXi9ixvP+hgUZPPZCRwMNHgY2I0gkSeoAKAUuysDJhWDmrygwfGdlkbGmmtW6PQjtMYFx69Igt5btvhjEBoJTw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.62.0.tgz", + "integrity": "sha512-V7rDgbxViiHU0OpcFEDp3l41IFvWTasKHfXw8SQ6yIgtZ8VpFqmz2TR5N7X85iIOmWIvK5HV0yp0eDdsly0+rA==", "license": "MIT", "dependencies": { "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.60.0", - "@sentry/opentelemetry": "10.60.0", + "@sentry/core": "10.62.0", + "@sentry/opentelemetry": "10.62.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -9208,22 +9219,22 @@ } }, "node_modules/@sentry/node-core/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/opentelemetry": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.60.0.tgz", - "integrity": "sha512-gl+2NVH+9RmTu7pd9kV1tKif+Th+p9tmnXR1l3Sb3Wqo1ir5FaNMKrloWEKMXjnepii9EJUrEHdSC+i8NoexxQ==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.62.0.tgz", + "integrity": "sha512-nFwBgtjfwgY8P5lAuQFWfAsQW1MXxuQ6kR/HtBs+A6julqwGGS2QnQ65OCWMzz6IqDEL/pRgT1405/gU+OXU3A==", "license": "MIT", "dependencies": { "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.60.0" + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" @@ -9235,22 +9246,22 @@ } }, "node_modules/@sentry/opentelemetry/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.60.0.tgz", - "integrity": "sha512-ALRIDOj7T1Vk9t9IyI12Tq2t3MKoV2F/mKn140AiUBk3WzQhq2gXQUFpcsaDYA2FBsrYoYc6qdqrny6mMbA0Xg==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.62.0.tgz", + "integrity": "sha512-PChimVpY0wzs3H/hJqyl87/ITTHwIZWTSY68QoENZyLnp7DvLcFiZYub/gFws1pzDPhtIQXVLU72fbmUjT5PSg==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.60.0", - "@sentry/core": "10.60.0" + "@sentry/browser": "10.62.0", + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" @@ -9260,69 +9271,69 @@ } }, "node_modules/@sentry/react/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/replay": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.60.0.tgz", - "integrity": "sha512-j+w774BP1p+v/ga1hJAJSn0cXgTiB2VWwQCAxukF7AEXscny/OCXzTTsaPxdovw9kDHI641vKV+/yixLV2nKIQ==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.62.0.tgz", + "integrity": "sha512-rWp4hBhZOmdQhisxcKzAwTGiRk/LvWnNaElWe7nbRhjsM/usp2095yfjq4iJ47v9MtO7xxY6eUz++fLBycqXKg==", "license": "MIT", "dependencies": { - "@sentry/browser-utils": "10.60.0", - "@sentry/core": "10.60.0" + "@sentry/browser-utils": "10.62.0", + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/replay-canvas": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.60.0.tgz", - "integrity": "sha512-mYyQRJbhRRaqUkRvkJZyqt9AWdomh4108LOAph1YJvV1jW7tKYPPFNAUveJd7YkYCyhUOtekN0DKNJveK802qA==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.62.0.tgz", + "integrity": "sha512-CzPAxmpe5US/ABGA1TzpjFKOFZN5uqlzrRh/uM9/daVuzLVKIAQ0XRNxo/PPEXvlDm/PoMdI5L0qIODuIKnyyw==", "license": "MIT", "dependencies": { - "@sentry/core": "10.60.0", - "@sentry/replay": "10.60.0" + "@sentry/core": "10.62.0", + "@sentry/replay": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/replay-canvas/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/replay/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/server-utils": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.60.0.tgz", - "integrity": "sha512-SX+MzWM3nz5ttKT48rlfktm0ERyIpDLma+b6pYeWgW2oFHKcpIu0g0qMGJrZs4lKM3MlgV7IqLa4texMqTp9kQ==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.62.0.tgz", + "integrity": "sha512-S5szsj6kKBhxw97b2HA98fYp/PpWXvSizlisEzb2rnL4IH6RAJ8wP05/fnth8pSywTH+gtUu+i6Wn8e8rX5HvA==", "license": "MIT", "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", "@apm-js-collab/tracing-hooks": "^0.10.0", "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.60.0", + "@sentry/core": "10.62.0", "magic-string": "~0.30.0" }, "engines": { @@ -9330,9 +9341,9 @@ } }, "node_modules/@sentry/server-utils/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" @@ -9362,22 +9373,22 @@ } }, "node_modules/@sentry/vercel-edge": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.60.0.tgz", - "integrity": "sha512-Ti7LR4Fm61IBSMQsOE9IkM//yQqffg7UUl898geqXEN3hq50J79OwNENAjDsXwsrP90220pHGj1rT+Dsrci6iw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.62.0.tgz", + "integrity": "sha512-p9gtIRywc6X2JOSh8WGLXCbY8969EDIF+dcejGbbQO9ekRzyREcJIxbJxfofUsynVO6dpEUi5Eayl71JhYwcPQ==", "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/core": "10.60.0" + "@sentry/core": "10.62.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/vercel-edge/node_modules/@sentry/core": { - "version": "10.60.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.60.0.tgz", - "integrity": "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==", + "version": "10.62.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz", + "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==", "license": "MIT", "engines": { "node": ">=18" @@ -10413,9 +10424,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10433,9 +10441,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10453,9 +10458,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10473,9 +10475,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10493,9 +10492,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10513,9 +10509,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -10824,9 +10817,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -11563,9 +11556,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11580,9 +11570,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11597,9 +11584,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11614,9 +11598,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11631,9 +11612,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11648,9 +11626,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11665,9 +11640,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11682,9 +11654,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11699,9 +11668,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11716,9 +11682,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11813,34 +11776,29 @@ ] }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, "node_modules/@vitest/coverage-v8": { @@ -23347,6 +23305,267 @@ } } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -23594,37 +23813,28 @@ } }, "node_modules/lint-staged": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", - "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", + "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.3", - "listr2": "^9.0.5", - "picomatch": "^4.0.3", + "listr2": "^10.2.1", + "picomatch": "^4.0.4", "string-argv": "^0.3.2", - "tinyexec": "^1.0.4", - "yaml": "^2.8.2" + "tinyexec": "^1.2.4" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=20.17" + "node": ">=22.22.1" }, "funding": { "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" + }, + "optionalDependencies": { + "yaml": "^2.9.0" } }, "node_modules/listenercount": { @@ -23634,21 +23844,20 @@ "license": "ISC" }, "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", + "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.13.0" } }, "node_modules/loader-runner": { @@ -23916,6 +24125,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -23933,6 +24149,24 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -23949,6 +24183,24 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -25465,9 +25717,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT" }, @@ -28781,6 +29033,50 @@ "node": ">=10.0.0" } }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -30934,9 +31230,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -30944,9 +31240,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -32460,24 +32756,23 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -32486,14 +32781,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -32502,13 +32798,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -32570,490 +32869,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/vitest": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", @@ -33851,18 +33666,18 @@ } }, "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -33945,31 +33760,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8e74e262..0441be76 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,8 +60,8 @@ "@mui/icons-material": "^9.1.1", "@mui/material": "^9.1.2", "@mui/material-nextjs": "^9.1.1", - "@mui/x-date-pickers": "^9.6.0", - "@sentry/nextjs": "^10.60.0", + "@mui/x-date-pickers": "^9.7.0", + "@sentry/nextjs": "^10.61.0", "@stripe/react-stripe-js": "^6.6.0", "@stripe/stripe-js": "^9.8.0", "@tanstack/react-query": "^5.101.1", @@ -113,7 +113,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@types/seedrandom": "^3.0.8", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^6.0.3", "@vitest/coverage-v8": "^4.1.9", "axe-core": "^4.11.4", "cross-env": "^10.1.0", @@ -131,7 +131,7 @@ "knip": "^5.88.1", "license-checker": "^25.0.1", "linkinator": "^7.6.1", - "lint-staged": "^16.4.0", + "lint-staged": "^17.0.8", "madge": "^8.0.0", "msw": "^2.14.6", "msw-storybook-addon": "^2.0.7", @@ -141,6 +141,7 @@ "storybook": "^10.4.6", "tsx": "^4.22.4", "typescript": "^6.0.3", + "vite": "^8.1.0", "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.9", "vitest-axe": "^1.0.0-pre.5" diff --git a/specs/specification-backend.md b/specs/specification-backend.md index a94919d8..05b73c05 100644 --- a/specs/specification-backend.md +++ b/specs/specification-backend.md @@ -61,7 +61,7 @@ Um handler por use case, organizado em `Application/UseCases///`. - **Notificação por tier**: `IPlanoNotificationPolicy` (Application/Interfaces): `Task ResolverPorTreinadorAsync(treinadorId, ct)` / `ResolverPorAlunoAsync(alunoId, ct)`. Record `CanaisNotificacao(bool Email, bool WhatsApp)` (`CanaisNotificacao.Nenhum` = `(false,false)`). Impl `PlanoNotificationPolicy` (Infrastructure/Notifications/): resolve treinador → `PlanoPlataformaId` → plano → `TierPlanoExtensions`; resolve aluno → vínculo ativo → assinatura atual → treinador → plano; sem plano = `Nenhum`. Registrado `AddScoped` no DI. Cross-ref `TierPlanoExtensions` [specification-model]. - Health: `IHealthReportCollector`, `IHealthReportSender`. - App services: `ILimiteTreinadorService`. -- Repositórios (`Interfaces/Repositories/`, contagem ancorada por `Tests/Architecture/SpecInventoryTests`): Conta, Aluno, Treino, Exercicio, GrupoMuscular, TreinoAluno, ExecucaoTreino, SystemUser, Treinador, PlanoPlataforma, Pacote, VinculoTreinadorAluno, LogAprovacao, TokenRevogado, PasswordResetToken, EmailVerificationToken, EmailDeliveryLog, WhatsAppDeliveryLog, AssinaturaAluno, Pagamento, AssinaturaTreinador, PagamentoTreinador, Assinante, ContaRecebimento, HealthReportConfig, HealthSnapshot, ErrorLog, AdminStats, Outbox, MensagemSuporte, RefreshToken, RefreshTokenFamily, NotaFiscal, ContaMfa, MfaRecoveryCode, MfaChallenge, TrustedDevice, TrocaEmailToken. Implementações em `Infrastructure/Persistence/Repositories`. `IMensagemSuporteRepository`: `AdicionarAsync` + `ExcluirPorContaIdAsync` (ExecuteDelete, usado na anonimização LGPD). `IRefreshTokenFamilyRepository`: `LimparExpiradasAsync` (GC famílias revogadas/pós-absoluto) + `ExcluirPorContaIdAsync` (purga LGPD, cascade nos tokens). +- Repositórios (`Interfaces/Repositories/`, contagem ancorada por `Tests/Architecture/SpecInventoryTests`): Conta, Aluno, Treino, Exercicio, GrupoMuscular, TreinoAluno, ExecucaoTreino, SystemUser, Treinador, PlanoPlataforma, Pacote, VinculoTreinadorAluno, LogAprovacao, TokenRevogado, PasswordResetToken, EmailVerificationToken, EmailDeliveryLog, WhatsAppDeliveryLog, AssinaturaAluno, Pagamento, AssinaturaTreinador, PagamentoTreinador, Assinante, ContaRecebimento, HealthReportConfig, HealthSnapshot, ErrorLog, AdminStats, Outbox, MensagemSuporte, RefreshToken, RefreshTokenFamily, NotaFiscal, ContaMfa, MfaRecoveryCode, MfaChallenge, TrustedDevice, TrocaEmailToken, ReconciliacaoStripeEstado. Implementações em `Infrastructure/Persistence/Repositories`. `IMensagemSuporteRepository`: `AdicionarAsync` + `ExcluirPorContaIdAsync` (ExecuteDelete, usado na anonimização LGPD). `IRefreshTokenFamilyRepository`: `LimparExpiradasAsync` (GC famílias revogadas/pós-absoluto) + `ExcluirPorContaIdAsync` (purga LGPD, cascade nos tokens). ### Services / Settings - `Application/Services/LimiteTreinadorService` (`ILimiteTreinadorService`): valida que treinador tem plano e que `vínculos ativos < plano.MaxAlunos`; senão lança `LimiteAlunosAtingidoException`. Usa `ICapacidadePlano` (domínio). @@ -132,7 +132,7 @@ Todos com rate `auth`. Postura/fatores em [specification-security] §2.1; modelo - `PaginacaoFilter`: valida query `pagina >= 1` e `1 <= tamanhoPagina <= 100`; senão 400. - `RequireAssinaturaAtivaFilterBase` (abstract, T8) — base compartilhada `IEndpointFilter` para filtros de inadimplência. **GETs liberados** (sem checar assinatura — preserva leitura/histórico em inadimplência; princípio LGPD/CDC). Implementações injetam `IUserContext`, delegam checagem a `EstaInadimplenteAsync(services, userContext, ct)` e retornam 403 ProblemDetails com `code=CodigoErro` (abstract) se inadimplente. - `RequireAssinaturaAtivaFilter : RequireAssinaturaAtivaFilterBase` (transient): impl aluno — `code=ASSINATURA_INADIMPLENTE`. `TipoConta != Aluno` → pass-through. Consulta `IAlunoRepository` + `IAssinaturaAlunoRepository` (última não-Cancelada). Cross-ref [specification-stripe]/[specification-frontend]. -- `RequireAssinaturaTreinadorAtivaFilter : RequireAssinaturaAtivaFilterBase` (transient): impl treinador — `code=ASSINATURA_TREINADOR_INADIMPLENTE`. `TipoConta != Treinador` → pass-through. Atualmente retorna false (placeholder; enforcement efetivo pendente de integração com `IAssinaturaTreinadorRepository`). Não-Treinador / sem assinatura passam direto. +- `RequireAssinaturaTreinadorAtivaFilter : RequireAssinaturaAtivaFilterBase` (transient): impl treinador — `code=ASSINATURA_TREINADOR_INADIMPLENTE`. `TipoConta != Treinador` → pass-through. Consulta `IAssinaturaTreinadorRepository.ObterAtualPorTreinadorAsync` (single-row); `Status==Inadimplente` → 403. Aplicado em endpoints de consumo de feature paga do treinador (lista em [specification-stripe] §Enforcement backend). Não-Treinador / sem assinatura passam direto. ### Middleware / error mapping `GlobalExceptionHandler` (`IExceptionHandler`, RFC7807 ProblemDetails): @@ -181,7 +181,7 @@ Todos anônimos + rate `internal`. Autenticação por header `X-Internal-Key` co ## 5. INFRASTRUCTURE LAYER ### EF Core / persistência -- `AppDbContext` schema-agnostic (sem `HasDefaultSchema`; schema vem do `Search Path` da connection), `UseSnakeCaseNamingConvention`, `MigrationsHistoryTable("__EFMigrationsHistory")`. Configs por entidade em `Persistence/Configurations` (`ApplyConfigurationsFromAssembly`). 40 DbSets (38 `public` — inclui `DataProtectionKeys` da infra de DataProtection, mapeada pela lib via `IDataProtectionKeyContext` sem Configuration própria — + 2 `internal` `TreinoExercicio`/`ExecucaoExercicio`, composição); `SerieConfig` mapeada SEM DbSet (acessada por navegação); `DadosFiscais`/`EnderecoFiscal` são owned VOs em `Treinador` (sem DbSet). Contagem de tabelas EF/NON-EF/migrations: canônica em [specification-db §STACK] (ancoragem `Tests/Architecture/SpecInventoryTests` — quebra força atualizar a spec db). Cross-ref [specification-db] (schema/migrations/enums). +- `AppDbContext` schema-agnostic (sem `HasDefaultSchema`; schema vem do `Search Path` da connection), `UseSnakeCaseNamingConvention`, `MigrationsHistoryTable("__EFMigrationsHistory")`. Configs por entidade em `Persistence/Configurations` (`ApplyConfigurationsFromAssembly`). 41 DbSets (39 `public` — inclui `DataProtectionKeys` da infra de DataProtection, mapeada pela lib via `IDataProtectionKeyContext` sem Configuration própria — + 2 `internal` `TreinoExercicio`/`ExecucaoExercicio`, composição); `SerieConfig` mapeada SEM DbSet (acessada por navegação); `DadosFiscais`/`EnderecoFiscal` são owned VOs em `Treinador` (sem DbSet). Contagem de tabelas EF/NON-EF/migrations: canônica em [specification-db §STACK] (ancoragem `Tests/Architecture/SpecInventoryTests` — quebra força atualizar a spec db). Cross-ref [specification-db] (schema/migrations/enums). - Registrado scoped em `InfrastructureExtensions` montando `DbContextOptions` na hora e passando o `IDomainEventDispatcher`. `IUnitOfWork` e `IDbContextTransactionProvider` resolvem para o MESMO `AppDbContext` scoped. - Repositórios (`Persistence/Repositories`): classe com ctor primário `(AppDbContext context)`, métodos async; leituras de listagem usam `AsNoTracking`; paginação `(IReadOnlyList Items, int Total)` com `Skip/Take`. Todos `AddScoped` em `InfrastructureExtensions`. diff --git a/specs/specification-db.md b/specs/specification-db.md index 6581eb36..111337e3 100644 --- a/specs/specification-db.md +++ b/specs/specification-db.md @@ -12,9 +12,9 @@ Notação coluna: `nome(tipo, NN|null[, nota])`. PK / FK(col→tabela, ONDELETE) - PostgreSQL 17 (Supabase). EF Core 10, snake_case naming convention. Stack macro da app em AGENTS.md §STACK. - Migrations SCHEMA-AGNOSTIC: `AppDbContext` SEM `HasDefaultSchema`. Schema-alvo vem do `search_path` da connection (ex.: `Search Path=homolog`). Mesmas migrations aplicam em qualquer schema. - **History table — RUNTIME pina o schema; DESIGN-TIME não** (gotcha Npgsql 8.0.11): `NpgsqlHistoryRepository.ExistsSql` checa a existência da `__EFMigrationsHistory` com schema HARDCODED em `public` (`n.nspname = TableSchema ?? "public"`), mas CREATE/leitura usam o search_path. Sem pinar, num alvo cujo `public` NÃO tem a history (ex.: dry-run clonando só `homolog`), o Exists dá falso-negativo → CREATE plano cai no search_path e colide (`42P07 already exists`). Por isso o **runtime** (`InfrastructureExtensions`, usado por `app migrate`/dry-run) pina `MigrationsHistoryTable("__EFMigrationsHistory", <1º schema do Search Path>)` via `MigrationHistorySchemaResolver` → Exists/CREATE/leitura no MESMO schema. O **design-time** (`AppDbContextFactory`) fica SEM schema (unqualified) de propósito: `dotnet ef migrations script` precisa gerar SQL portável e reusável por schema (§APLICAÇÃO DE MIGRATIONS depende de unqualified + `SET search_path`). -- Schemas com estrutura IDÊNTICA: `homolog` (homologação, deploy ativo canônico), `develop` (sandbox), `public` (**PRODUÇÃO** — dados reais de usuários). **SAFETY**: `public` é PRODUÇÃO, NÃO sandbox apesar do nome default do Postgres — NUNCA semear conta de teste, rodar DML ad-hoc nem apontar fixture E2E p/ `public`. Toda escrita de teste/seed vai p/ `homolog`. Qualificar SEMPRE o schema (`homolog.`) ou pinar `Search Path=homolog` antes de DML manual; o ownership por postgres em `public` (vs forzion_api em homolog/develop) é a 2ª linha de defesa, não a 1ª. 43 tabelas BASE cada (41 entidades EF + `ai_token_usage` não-EF + `__EFMigrationsHistory` de controle) após todas as migrations aplicadas (inclui MFA, fiscal e DataProtection, já merjadas). Nível atual por schema pode divergir — conferir antes (§APLICAÇÃO DE MIGRATIONS §GOTCHA). +- Schemas com estrutura IDÊNTICA: `homolog` (homologação, deploy ativo canônico), `develop` (sandbox), `public` (**PRODUÇÃO** — dados reais de usuários). **SAFETY**: `public` é PRODUÇÃO, NÃO sandbox apesar do nome default do Postgres — NUNCA semear conta de teste, rodar DML ad-hoc nem apontar fixture E2E p/ `public`. Toda escrita de teste/seed vai p/ `homolog`. Qualificar SEMPRE o schema (`homolog.`) ou pinar `Search Path=homolog` antes de DML manual; o ownership por postgres em `public` (vs forzion_api em homolog/develop) é a 2ª linha de defesa, não a 1ª. 44 tabelas BASE cada (42 entidades EF + `ai_token_usage` não-EF + `__EFMigrationsHistory` de controle) após todas as migrations aplicadas (inclui MFA, fiscal e DataProtection, já merjadas). Nível atual por schema pode divergir — conferir antes (§APLICAÇÃO DE MIGRATIONS §GOTCHA). - `ai_token_usage`: existe nos 3 schemas mas NÃO é gerenciada por migration EF (criada fora do EF). Recriar via `CREATE TABLE .ai_token_usage (LIKE homolog.ai_token_usage INCLUDING ALL)`. -- 49 migrations EF (arquivos não-Designer/Snapshot em `Infrastructure/Migrations/`; última por timestamp `AdicionarConcurrencyTokenPagamentos`). Tabela de controle `__EFMigrationsHistory` por schema (colunas snake_case: `migration_id` varchar(150) PK, `product_version` varchar(32); EF `ProductVersion` = versão do pacote em `forzion.tech.Infrastructure.csproj`, não fixar aqui). +- 50 migrations EF (arquivos não-Designer/Snapshot em `Infrastructure/Migrations/`; última por timestamp `CriarReconciliacaoStripeEstado`). Tabela de controle `__EFMigrationsHistory` por schema (colunas snake_case: `migration_id` varchar(150) PK, `product_version` varchar(32); EF `ProductVersion` = versão do pacote em `forzion.tech.Infrastructure.csproj`, não fixar aqui). - `AdicionarConcurrencyTokenTreinador`: mapeia o system column `xmin` de `treinadores` como concurrency token (concorrência otimista). `AdicionarConcurrencyTokenPagamentos`: idem para `pagamentos` + `pagamentos_treinador` (configs `PagamentoConfiguration.cs:16`, `PagamentoTreinadorConfiguration.cs:16`). NÃO geram DDL — o `AddColumn` no `.cs` é artefato de modelo; o SQL gerado só insere a linha de history. Aplicar é no-op estrutural (só registra a migration). ## APLICAÇÃO DE MIGRATIONS (multi-schema — descobertas operacionais) @@ -137,6 +137,8 @@ assinaturas_treinador — assinatura recorrente do plano da plataforma (treinado pagamentos_treinador — cobranças do plano do treinador (PaymentIntent direto-plataforma, sem Connect). id(uuid,NN); treinador_id(uuid,NN); assinatura_treinador_id(uuid,NN); valor(numeric,NN); status(text,NN,PagamentoStatus); metodo_pagamento(text,NN,default Pix,MetodoPagamento); finalidade(text,NN,FinalidadePagamentoTreinador); plano_alvo_id(uuid,null,plano da troca); stripe_payment_intent_id(varchar,null); client_secret(varchar,null); pix_qr_code(text,null); pix_qr_code_url(varchar,null); pix_expiracao(tstz,null); data_pagamento(tstz,null); created_at(NN); updated_at(null). PK(id) FK(treinador_id→treinadores,RESTRICT) FK(assinatura_treinador_id→assinaturas_treinador,RESTRICT) UQ(stripe_payment_intent_id) UQ(assinatura_treinador_id WHERE status='Pendente'). Concurrency token = system column `xmin` (migration `AdicionarConcurrencyTokenPagamentos`). +reconciliacao_stripe_estado — cursor singleton (high-water-mark) da reconciliação Stripe. id(uuid,NN); ultimo_evento_reconciliado_utc(tstz,NN,maior `event.created` já processado); created_at(NN); updated_at(null). PK(id). 1 linha por schema; `AvancarCursor` é monotônico (nunca retrocede). Migration `CriarReconciliacaoStripeEstado`. + ### Fiscal (NFS-e) notas_fiscais — nota fiscal de serviço da receita da forzion (fluxo 1: assinatura SaaS do treinador; fluxo 2: comissão marketplace mensal agregada). id(uuid,NN); treinador_id(uuid,NN); tipo(text,NN,TipoNotaFiscal); pagamento_treinador_id(uuid,null,só fluxo 1); competencia_inicio(date,null,só fluxo 2); competencia_fim(date,null,só fluxo 2); valor(numeric(10,2),NN,ck `valor >= 0`); status(text,NN,NotaFiscalStatus); chave_acesso(varchar100,null); numero_nfse(varchar100,null); numero_dps(varchar100,null,série+nº estável p/ idempotência); data_emissao(tstz,null); danfse_ref(varchar500,null,ref/URL do PDF); codigo_erro(varchar100,null); motivo_erro(varchar2000,null); cancelamento_pendente_pre_emissao(bool,NN,default false,migration `AdicionarCancelamentoPendentePreEmissaoNfse`); motivo_cancelamento_pendente(varchar500,null); created_at(NN); updated_at(null). PK(id) FK(treinador_id→treinadores,RESTRICT) FK(pagamento_treinador_id→pagamentos_treinador,RESTRICT,null) idx(treinador_id) UQ(pagamento_treinador_id WHERE pagamento_treinador_id IS NOT NULL,idempotência fluxo 1) UQ(treinador_id,tipo,competencia_inicio WHERE competencia_inicio IS NOT NULL,idempotência fluxo 2). diff --git a/specs/specification-stripe.md b/specs/specification-stripe.md index 306c7811..14f5243b 100644 --- a/specs/specification-stripe.md +++ b/specs/specification-stripe.md @@ -39,6 +39,8 @@ A idem-key do PaymentIntent é DETERMINÍSTICA e montada pelo CALLER, não deriv - troca de plano: `cobr:troca:{assinatura.Id}:{novoPlano.Id}:{minuto}` — `TrocarPlanoTreinadorHandler` (upgrade + inadimplente). `novoPlano.Id` discrimina trocas distintas no mesmo minuto. - **Defesa em 3 camadas (PERF-01)**: (1) idem-key determinística no Stripe (retry de transporte não duplica intent), (2) pré-check + re-check de pendente reusável na app (evita Stripe na reuse), (3) UNIQUE parcial `pagamentos[_treinador] WHERE status='Pendente'` no banco (corrida real → 23505 → reuse). Ver §COMPONENTES (`CriarPagamentoComIntentService`) e [specification-concurrency §4]. - **RESÍDUO ACEITO — crash cruzando o minuto** (E1, decisão do usuário 2026-06-18): como o bucket é de minuto e o intent é criado ANTES do commit, um crash entre `CriarIntent` e `CommitAsync` recuperado em minuto DIFERENTE muda a key → 2º intent (Pix: 2 QRs pagáveis). Janela ~ms, sev BAIXA; aceita conscientemente (sem migration/escrita extra no caminho feliz). NÃO eliminada pelas 3 camadas (o pendente só é reusável se commitou; no crash não commitou). Hardening futuro: persistir a idem-key na row e reusá-la pós-crash. Detalhe/3 eixos em [specification-concurrency §1]. +- **RESÍDUO ACEITO — double-tap cruzando boundary de minuto** (BI-4, auditoria 2026-06-25): 2 requests concorrentes com `GetUtcNow()` em lados opostos do boundary de minuto → buckets distintos → keys distintas → Stripe cria 2 intents; só 1 row sobrevive ao UNIQUE parcial, o perdedor reusa a row vencedora, deixando o 2º intent órfão (2º QR Pix pagável). Mesma classe do E1 acima (sub-segundo, sev BAIXA); defesa em camadas (UNIQUE parcial + pre-check) cobre o caso real. Aceito junto do E1; hardening futuro idêntico. Ver [specification-concurrency §1]. +- **RESÍDUO ACEITO — reuse de Pendente ignora `Metodo`/`Valor`** (BI-5, auditoria 2026-06-25): `VerificarIdempotencia` reusa um `Pagamento` Pendente com intent setado sem comparar `MetodoPagamento` nem `Valor` — first-wins. Pendente Pix + retry pedindo Cartão (ou valor diferente) recebe o intent original. NÃO é duplo-cobro; o Pendente original é fonte de verdade até expirar/resolver. Aceito conscientemente. ## CONFIG (chaves) | Chave | Onde lida | Função | Ausente | @@ -56,6 +58,7 @@ A idem-key do PaymentIntent é DETERMINÍSTICA e montada pelo CALLER, não deriv - Local (dev): User Secrets ou `.env` na raiz (compose pega `STRIPE_SECRET_KEY`, etc). - Deploy: env vars no compose → `Stripe__SecretKey`/`Stripe__WebhookSecret`/`Stripe__UrlBase` ← `${STRIPE_*}` (vêm do `/opt/forzion/.env` na VM). `Internal__ApiKey` ← `${INTERNAL_API_KEY}`. - ValidateOnStart enforça: SecretKey/WebhookSecret não-vazios, TaxaPlataformaPercent ∈ (0,100]. +- **Decisão SPH-09 — taxa == 100% (auditoria 2026-06-25)**: verificado via Stripe docs (`context7`) — erro `off_session_payment_application_fee_amount_too_high` = "application_fee_amount must be ≤ amount". Stripe ACEITA `fee == amount`. Logo `TaxaPlataformaPercent ∈ (0,100]` é o bound Stripe-correto; `> 100` ⇒ `fee > amount` ⇒ Stripe rejeita. `taxa == 100` (treinador líquido zero) é misconfig de negócio mas Stripe-válido — documentado como resíduo, NÃO bloqueado no boot. ## COMPONENTES - `StripeService` (Infrastructure/Services) — implementa `IStripeService`. Wrapper fino sobre Stripe.net SDK; sem retry custom. @@ -115,6 +118,7 @@ Cobrança do treinador pelo próprio plano (cadastro/renovação/troca) — NÃO - **Roteamento charge.refunded / charge.dispute.created (T4)**: handler tenta primeiro carregar `Pagamento` (aluno) por `PaymentIntentId`; não encontrado → tenta `PagamentoTreinador`. Ambos os tipos são tratados: `ProcessarEstornoTreinadorAsync` e `ProcessarDisputaTreinadorAsync` chamam `MarcarEstornado`/`MarcarEmDisputa` no `PagamentoTreinador` + `assinatura.MarcarInadimplentePorDisputa` na `AssinaturaTreinador`. - **FinalizarCadastroAsync — retry por exceção (T1)**: quando `Finalidade==Cadastro` e `payment_intent.succeeded` chega, `FinalizarCadastroAsync` é chamado atomicamente ANTES do commit. Qualquer falha de domínio (`Ativar`/`ConfirmarPagamentoPlano`) ou ausência de entidade (assinatura/treinador/conta não encontrados) **lança `InvalidOperationException`** — impede o `CommitAsync`, força 500 na resposta do webhook, e o Stripe retenta. Garante que `PagamentoTreinador.Pago` nunca é persistido sem assinatura ativada (sem estado parcial). - **Cross-account defense**: `ValidarConnectAccountAsync` compara `event.AccountId` vs `ContaRecebimento.StripeConnectAccountId` do treinador dono da assinatura. **Distinção connect-null vs mismatch (T2)**: (a) `ContaRecebimento` existe mas `StripeConnectAccountId` é null → **lança `InvalidOperationException`** (drift de configuração: aluno pagou via account mas treinador sem Connect — nunca deve ocorrer; força 500 + retry Stripe para não deixar pagamento preso em Pendente); (b) mismatch de account → log warning + retorna `JaConsistente` (defesa contra replay de webhook de outro account). **Retorno (T3)**: `ValidarConnectAccountAsync` retorna `(bool Valido, AssinaturaAluno? Assinatura)` — quando válido, entrega a `AssinaturaAluno` já carregada (evita segundo round-trip no `ProcessarPagamentoPagoAsync`/`FalhouAsync`). Não aplicada em `charge.refunded`/`charge.dispute.created` (sem vetor útil — refund/dispute invertem dinheiro do próprio destino). +- **RESÍDUO ACEITO — cross-account skip em refund/dispute** (WH-4, auditoria 2026-06-25): `charge.refunded` e `charge.dispute.created` não executam `ValidarConnectAccountAsync` — casam o evento pelo `paymentIntentId` (único globalmente). Eventos de contas conectadas sem row `Pagamento`/`PagamentoTreinador` correspondente caem em `JaConsistente` (log warn). Omissão DELIBERADA: unicidade do PI id torna o match suficiente; sem vetor útil cross-account nesses eventos. ## ENDPOINTS | Método/Rota | Auth | Handler | Sucesso | Erros | @@ -221,8 +225,8 @@ Disparado por `billing-reconciliation.yml` (cron semanal seg 04h UTC + `workflow ### Enforcement backend - `RequireAssinaturaAtivaFilterBase` (`forzion.tech.Api/Filters/`) — base abstrata `IEndpointFilter` (T8). **GETs liberados automaticamente** (preserva histórico/LGPD — sem checar assinatura em leitura). Injeta `IUserContext` via `RequestServices`; delega checagem a `EstaInadimplenteAsync(services, userContext, ct)` (abstract). Se inadimplente → **403 ProblemDetails** com `code = CodigoErro` (abstract), title `"Assinatura inadimplente"`, detail `"Regularize seu pagamento para continuar usando esta funcionalidade."`. - `RequireAssinaturaAtivaFilter : RequireAssinaturaAtivaFilterBase` — impl aluno (`CodigoErro = "ASSINATURA_INADIMPLENTE"`). `TipoConta != Aluno` → false. Resolve aluno via `IAlunoRepository.ObterPorContaIdAsync`, depois `IAssinaturaAlunoRepository.ObterAtualPorAlunoAsync` (single-row no DB: última não-Cancelada por `CreatedAt`). Inadimplente → 403. (Era `ListarPorAlunoAsync` + `MaxBy` em memória — caminho QUENTE; trocado na remediação 2026-06-24, ver [specification-performance] §1 "Filtro no banco".) -- `RequireAssinaturaTreinadorAtivaFilter : RequireAssinaturaAtivaFilterBase` — impl treinador (`CodigoErro = "ASSINATURA_TREINADOR_INADIMPLENTE"`). `TipoConta != Treinador` → false. Atualmente retorna false (placeholder — billing treinador implementa consulta a `IAssinaturaTreinadorRepository` aqui quando enforcement ativo). -- Aplicado em: `POST /aluno/execucoes` (registrar nova execução de treino). Expandir conforme necessidade — princípio: bloquear **consumo de feature paga**, NUNCA bloquear leitura (LGPD/CDC: histórico que já pagou deve continuar visível) nem `/aluno/pagamentos` (precisa pra regularizar). +- `RequireAssinaturaTreinadorAtivaFilter : RequireAssinaturaAtivaFilterBase` — impl treinador (`CodigoErro = "ASSINATURA_TREINADOR_INADIMPLENTE"`). `TipoConta != Treinador` → false (aluno/admin bypassam). Resolve `IAssinaturaTreinadorRepository.ObterAtualPorTreinadorAsync(PerfilId)` (single-row no DB) → `Status == Inadimplente` → 403. +- Aplicado em (consumo de feature paga do treinador): aluno → `POST /aluno/execucoes`. Treinador → `POST /treinador/vinculos/{id}/aprovar`, `POST /treinador/alunos/{alunoId}/reativar`, `POST /treinador/alunos/{alunoId}/fichas/{treinoId}`, `POST /treinos`, `POST /treinos/{id}/vincular-aluno`, `POST /treinos/{id}/duplicar`. Princípio: bloquear **consumo/entrega de feature paga** (aceitar/servir aluno + criar/atribuir ficha), NUNCA leitura (GET liberado pela base — LGPD/CDC) nem regularização: `/treinador/plano*`, `/treinador/pagamentos*`, `/treinador/onboarding*`, `/treinador/dados-fiscais`, `/treinador/notas-fiscais*`, `modo-pagamento`, `pacotes` (setup comercial) e `desvincular` (remove acesso, não consome) ficam liberados. - Ambos Transient em DI (`forzion.tech.Api/Extensions/DependencyInjectionExtensions.cs`). ### UI bloqueio (frontend) @@ -287,3 +291,4 @@ Disparado por `billing-reconciliation.yml` (cron semanal seg 04h UTC + `workflow - Pix `ExpiresAfterSeconds=3600` (1h). Stripe envia `payment_intent.canceled` ao expirar — único caminho pra MarcarExpirado. - Sem cleanup proativo de pagamentos zombie (pendente sem StripePaymentIntentId após crash). `GerarCobrancaMensal`/`IniciarPagamentoPlano` re-uso de pendente lida com isso: marca Falhou + cria novo na próxima tentativa (idempotente via tx serializable). - Test/Live + WebhookSecret por-endpoint: ver §Stripe Test Mode vs Live Mode (canônico). +- Currency `"brl"` e fator ×100 hardcoded em todos os `Criar*PaymentIntent` (MN-5). Correto para BR-only (`Country="BR"` no Connect Express) — sem multi-currency por design. Centralizar se o escopo mudar.