From 8716aedc55aea20ce8778e2eab0983b76d50692a Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Sun, 23 Aug 2026 17:22:58 -0300 Subject: [PATCH 1/4] Start work for #326 From ad2eacdcf4bdef89cb51bea98b96735753b93db2 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Sun, 23 Aug 2026 17:37:59 -0300 Subject: [PATCH 2/4] Remove Pro gate from goals --- src/Orbit.Application/Common/AppConfigKeys.cs | 1 - .../Common/PayGateService.cs | 20 +- .../Goals/Commands/CreateGoalCommand.cs | 5 - .../Goals/Commands/DeleteGoalCommand.cs | 5 - .../Goals/Commands/LinkHabitsToGoalCommand.cs | 5 - .../Goals/Commands/ReorderGoalsCommand.cs | 5 - .../Goals/Commands/RestoreGoalCommand.cs | 5 - .../Goals/Commands/UpdateGoalCommand.cs | 5 - .../Commands/UpdateGoalProgressCommand.cs | 5 - .../Goals/Commands/UpdateGoalStatusCommand.cs | 5 - .../Goals/Queries/GetGoalByIdQuery.cs | 5 - .../Goals/Queries/GetGoalDetailQuery.cs | 5 - .../Goals/Queries/GetGoalMetricsQuery.cs | 5 - .../Queries/GetGoalProgressHistoryQuery.cs | 9 +- .../Goals/Queries/GetGoalReviewQuery.cs | 2 +- .../Goals/Queries/GetGoalsQuery.cs | 5 - .../Habits/Commands/CreateHabitCommand.cs | 7 - .../Commands/LinkGoalsToHabitCommand.cs | 5 - .../Habits/Commands/UpdateHabitCommand.cs | 7 - .../Commands/ApplyOnboardingCommand.cs | 11 +- .../Interfaces/IPayGateService.cs | 13 +- ...60823203458_RemoveGoalsProGate.Designer.cs | 2643 +++++++++++++++++ .../20260823203458_RemoveGoalsProGate.cs | 42 + .../Migrations/OrbitDbContextModelSnapshot.cs | 1 - .../Persistence/OrbitDbContext.cs | 2 +- .../AgentCatalogService.Capabilities.cs | 6 - .../Caching/GoalAiCacheInvalidationTests.cs | 4 +- .../Goals/CreateGoalCommandHandlerTests.cs | 20 +- .../Goals/DeleteGoalCommandHandlerTests.cs | 19 +- .../LinkHabitsToGoalCommandHandlerTests.cs | 22 +- .../Goals/ReorderGoalsCommandHandlerTests.cs | 20 +- .../Goals/RestoreGoalCommandHandlerTests.cs | 17 +- .../Goals/UpdateGoalCommandHandlerTests.cs | 20 +- .../UpdateGoalProgressCommandHandlerTests.cs | 19 +- .../UpdateGoalStatusCommandHandlerTests.cs | 19 +- .../Habits/CreateHabitCommandHandlerTests.cs | 2 - .../LinkGoalsToHabitCommandHandlerTests.cs | 5 +- .../Habits/UpdateHabitCommandHandlerTests.cs | 2 - .../Habits/UpdateHabitGoalSyncTests.cs | 2 - .../ApplyOnboardingCommandHandlerTests.cs | 13 +- .../Common/PayGateServiceTests.cs | 27 +- .../Goals/GetGoalByIdQueryHandlerTests.cs | 19 +- .../Goals/GetGoalDetailQueryHandlerTests.cs | 19 +- .../Goals/GetGoalMetricsQueryHandlerTests.cs | 18 - ...GetGoalProgressHistoryQueryHandlerTests.cs | 19 +- .../Goals/GetGoalReviewQueryHandlerTests.cs | 14 +- .../Goals/GetGoalsQueryHandlerTests.cs | 24 +- .../ApplyOnboardingConcurrencyTests.cs | 1 - .../Persistence/ConcurrencyRetryTests.cs | 8 - .../Services/AgentPolicyEvaluatorTests.cs | 8 +- .../Services/GoalCompletionServiceTests.cs | 13 - 51 files changed, 2730 insertions(+), 453 deletions(-) create mode 100644 src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.Designer.cs create mode 100644 src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.cs diff --git a/src/Orbit.Application/Common/AppConfigKeys.cs b/src/Orbit.Application/Common/AppConfigKeys.cs index e9d5a0d4..5b871c41 100644 --- a/src/Orbit.Application/Common/AppConfigKeys.cs +++ b/src/Orbit.Application/Common/AppConfigKeys.cs @@ -14,7 +14,6 @@ public static class AppConfigKeys public const string DailySummaryProOnly = "DailySummaryProOnly"; public const string SmartRescheduleProOnly = "SmartRescheduleProOnly"; public const string RetrospectiveProOnly = "RetrospectiveProOnly"; - public const string GoalsProOnly = "GoalsProOnly"; public const string MinSupportedVersion = "MinSupportedVersion"; public const string RequireApiKeyCreationStepUp = "RequireApiKeyCreationStepUp"; } diff --git a/src/Orbit.Application/Common/PayGateService.cs b/src/Orbit.Application/Common/PayGateService.cs index 5cd499dc..5e106873 100644 --- a/src/Orbit.Application/Common/PayGateService.cs +++ b/src/Orbit.Application/Common/PayGateService.cs @@ -145,21 +145,8 @@ public async Task CanUseRetrospective(Guid userId, CancellationToken ct return Result.Success(); } - public async Task CanAccessGoals(Guid userId, CancellationToken ct = default) - { - var user = await userRepository.GetByIdAsync(userId, ct); - if (user is null) - return Result.Failure(ErrorMessages.UserNotFound); - - var goalsProOnly = await appConfig.GetAsync(AppConfigKeys.GoalsProOnly, true, ct); - if (goalsProOnly && !user.HasProAccess) - return Result.PayGateFailure("Goals are a Pro feature. Upgrade to unlock!"); - - return Result.Success(); - } - - public Task CanCreateGoals(Guid userId, CancellationToken ct = default) => - CanAccessGoals(userId, ct); + public Task CanUseGoalReview(Guid userId, CancellationToken ct = default) => + RequireProAccess(userId, "Goal reviews are a Pro feature. Upgrade to unlock!", ct); public Task CanAccessCalendar(Guid userId, CancellationToken ct = default) => RequireProAccess(userId, "Calendar integration is a Pro feature. Upgrade to unlock!", ct); @@ -194,9 +181,6 @@ public Task CanManageUserFacts(Guid userId, CancellationToken ct = defau public Task CanUseSlipAlerts(Guid userId, CancellationToken ct = default) => RequireProAccess(userId, "Slip alerts are a Pro feature. Upgrade to unlock!", ct); - public Task CanLinkGoalsToHabits(Guid userId, CancellationToken ct = default) => - CanAccessGoals(userId, ct); - public async Task CanCreateApiKeys(Guid userId, CancellationToken ct = default) { return await CanManageApiKeys(userId, ct); diff --git a/src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs b/src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs index f821206b..9732ae51 100644 --- a/src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs +++ b/src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs @@ -25,7 +25,6 @@ public record CreateGoalCommand( public partial class CreateGoalCommandHandler( IGenericRepository goalRepository, IGenericRepository habitRepository, - IPayGateService payGate, IUserDateService userDateService, IGamificationService gamificationService, IGoalCompletionService goalCompletionService, @@ -35,10 +34,6 @@ public partial class CreateGoalCommandHandler( { public async Task> Handle(CreateGoalCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError(); - var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); if (request.Deadline is { } deadline && deadline < today) return Result.Failure(ErrorMessages.DeadlineInPast); diff --git a/src/Orbit.Application/Goals/Commands/DeleteGoalCommand.cs b/src/Orbit.Application/Goals/Commands/DeleteGoalCommand.cs index 319e5acd..58c5cb80 100644 --- a/src/Orbit.Application/Goals/Commands/DeleteGoalCommand.cs +++ b/src/Orbit.Application/Goals/Commands/DeleteGoalCommand.cs @@ -14,17 +14,12 @@ public record DeleteGoalCommand( public class DeleteGoalCommandHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUnitOfWork unitOfWork, IUserDateService userDateService, IMemoryCache cache) : IRequestHandler { public async Task Handle(DeleteGoalCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var goal = await goalRepository.FindOneTrackedAsync( g => g.Id == request.GoalId && g.UserId == request.UserId, cancellationToken: cancellationToken); diff --git a/src/Orbit.Application/Goals/Commands/LinkHabitsToGoalCommand.cs b/src/Orbit.Application/Goals/Commands/LinkHabitsToGoalCommand.cs index 4d0af6fb..a7225d44 100644 --- a/src/Orbit.Application/Goals/Commands/LinkHabitsToGoalCommand.cs +++ b/src/Orbit.Application/Goals/Commands/LinkHabitsToGoalCommand.cs @@ -18,17 +18,12 @@ public record LinkHabitsToGoalCommand( public class LinkHabitsToGoalCommandHandler( IGenericRepository goalRepository, IGenericRepository habitRepository, - IPayGateService payGate, IGoalCompletionService goalCompletionService, IUserDateService userDateService, IMemoryCache cache) : IRequestHandler { public async Task Handle(LinkHabitsToGoalCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - if (request.HabitIds.Count > AppConstants.MaxHabitsPerGoal) return Result.Failure(ErrorMessages.MaxHabitsPerGoal.Format(AppConstants.MaxHabitsPerGoal)); diff --git a/src/Orbit.Application/Goals/Commands/ReorderGoalsCommand.cs b/src/Orbit.Application/Goals/Commands/ReorderGoalsCommand.cs index ac4ae0c5..05234266 100644 --- a/src/Orbit.Application/Goals/Commands/ReorderGoalsCommand.cs +++ b/src/Orbit.Application/Goals/Commands/ReorderGoalsCommand.cs @@ -16,17 +16,12 @@ public record ReorderGoalsCommand( public class ReorderGoalsCommandHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUnitOfWork unitOfWork, IUserDateService userDateService, IMemoryCache cache) : IRequestHandler { public async Task Handle(ReorderGoalsCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var ids = request.Positions.Select(p => p.GoalId).ToHashSet(); var goals = await goalRepository.FindTrackedAsync( diff --git a/src/Orbit.Application/Goals/Commands/RestoreGoalCommand.cs b/src/Orbit.Application/Goals/Commands/RestoreGoalCommand.cs index 0dbdb0b4..0dabc396 100644 --- a/src/Orbit.Application/Goals/Commands/RestoreGoalCommand.cs +++ b/src/Orbit.Application/Goals/Commands/RestoreGoalCommand.cs @@ -14,17 +14,12 @@ public record RestoreGoalCommand( public class RestoreGoalCommandHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUnitOfWork unitOfWork, IUserDateService userDateService, IMemoryCache cache) : IRequestHandler { public async Task Handle(RestoreGoalCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var goals = await goalRepository.FindTrackedIgnoringFiltersAsync( g => g.Id == request.GoalId && g.UserId == request.UserId, cancellationToken); diff --git a/src/Orbit.Application/Goals/Commands/UpdateGoalCommand.cs b/src/Orbit.Application/Goals/Commands/UpdateGoalCommand.cs index 57c58b44..279e4836 100644 --- a/src/Orbit.Application/Goals/Commands/UpdateGoalCommand.cs +++ b/src/Orbit.Application/Goals/Commands/UpdateGoalCommand.cs @@ -21,7 +21,6 @@ public record UpdateGoalCommand( public class UpdateGoalCommandHandler( GoalRepositories repos, - IPayGateService payGate, IUserDateService userDateService, IGoalCompletionService goalCompletionService, IUnitOfWork unitOfWork, @@ -29,10 +28,6 @@ public class UpdateGoalCommandHandler( { public async Task Handle(UpdateGoalCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); if (request.Deadline is { } deadline && deadline < today) return Result.Failure(ErrorMessages.DeadlineInPast); diff --git a/src/Orbit.Application/Goals/Commands/UpdateGoalProgressCommand.cs b/src/Orbit.Application/Goals/Commands/UpdateGoalProgressCommand.cs index 111451b4..f7be2e4c 100644 --- a/src/Orbit.Application/Goals/Commands/UpdateGoalProgressCommand.cs +++ b/src/Orbit.Application/Goals/Commands/UpdateGoalProgressCommand.cs @@ -17,7 +17,6 @@ public record UpdateGoalProgressCommand( public class UpdateGoalProgressCommandHandler( GoalRepositories repos, - IPayGateService payGate, IGoalCompletionService goalCompletionService, IUnitOfWork unitOfWork, IUserDateService userDateService, @@ -25,10 +24,6 @@ public class UpdateGoalProgressCommandHandler( { public async Task Handle(UpdateGoalProgressCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var saved = await unitOfWork.ExecuteInTransactionAsync(async transactionToken => { var justCompleted = false; diff --git a/src/Orbit.Application/Goals/Commands/UpdateGoalStatusCommand.cs b/src/Orbit.Application/Goals/Commands/UpdateGoalStatusCommand.cs index 5087ff7b..a31c2608 100644 --- a/src/Orbit.Application/Goals/Commands/UpdateGoalStatusCommand.cs +++ b/src/Orbit.Application/Goals/Commands/UpdateGoalStatusCommand.cs @@ -17,7 +17,6 @@ public record UpdateGoalStatusCommand( public class UpdateGoalStatusCommandHandler( IGenericRepository goalRepository, - IPayGateService payGate, IGoalCompletionService goalCompletionService, IUnitOfWork unitOfWork, IUserDateService userDateService, @@ -25,10 +24,6 @@ public class UpdateGoalStatusCommandHandler( { public async Task Handle(UpdateGoalStatusCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var goal = await goalRepository.FindOneTrackedAsync( g => g.Id == request.GoalId && g.UserId == request.UserId, cancellationToken: cancellationToken); diff --git a/src/Orbit.Application/Goals/Queries/GetGoalByIdQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalByIdQuery.cs index d4b3b470..d2cf021f 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalByIdQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalByIdQuery.cs @@ -37,15 +37,10 @@ public record GetGoalByIdQuery( public class GetGoalByIdQueryHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUserDateService userDateService) : IRequestHandler> { public async Task> Handle(GetGoalByIdQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError(); - var loaded = await GoalDetailLoader.BuildGoalDetailAsync( goalRepository, userDateService, request.GoalId, request.UserId, cancellationToken); if (loaded is null) diff --git a/src/Orbit.Application/Goals/Queries/GetGoalDetailQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalDetailQuery.cs index 3dd34830..f3e59f8b 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalDetailQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalDetailQuery.cs @@ -18,15 +18,10 @@ public record GetGoalDetailQuery( public class GetGoalDetailQueryHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUserDateService userDateService) : IRequestHandler> { public async Task> Handle(GetGoalDetailQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError(); - var loaded = await GoalDetailLoader.BuildGoalDetailAsync( goalRepository, userDateService, request.GoalId, request.UserId, cancellationToken); if (loaded is null) diff --git a/src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs index 86a34c52..d4db053b 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs @@ -13,16 +13,11 @@ public record GetGoalMetricsQuery(Guid UserId, Guid GoalId) : IRequest goalRepository, - IPayGateService payGate, IUserDateService userDateService, IGoalProgressReadSyncer goalProgressReadSyncer) : IRequestHandler> { public async Task> Handle(GetGoalMetricsQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError(); - var userToday = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); var freshValues = await goalProgressReadSyncer.ComputeFreshValuesAsync( request.UserId, diff --git a/src/Orbit.Application/Goals/Queries/GetGoalProgressHistoryQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalProgressHistoryQuery.cs index a024b2f9..b37dd4cf 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalProgressHistoryQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalProgressHistoryQuery.cs @@ -18,19 +18,14 @@ public record GetGoalProgressHistoryQuery( /// /// Returns a goal's progress-log entries within a date range as an ascending series, for charting -/// progress over time. Pro-gated behind goals access; scoped to the requesting user's own goal. +/// progress over time, scoped to the requesting user's own goal. /// public class GetGoalProgressHistoryQueryHandler( IGenericRepository goalRepository, - IGenericRepository progressLogRepository, - IPayGateService payGate) : IRequestHandler> + IGenericRepository progressLogRepository) : IRequestHandler> { public async Task> Handle(GetGoalProgressHistoryQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError(); - var goalExists = await goalRepository.AnyAsync( g => g.Id == request.GoalId && g.UserId == request.UserId, cancellationToken); diff --git a/src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs index 637ef6a1..aa350c92 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs @@ -26,7 +26,7 @@ public async Task> Handle( GetGoalReviewQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); + var gateCheck = await payGate.CanUseGoalReview(request.UserId, cancellationToken); if (gateCheck.IsFailure) return gateCheck.PropagateError(); diff --git a/src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs b/src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs index 7a49e90c..3b2818db 100644 --- a/src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs +++ b/src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs @@ -38,16 +38,11 @@ public record GetGoalsQuery( public class GetGoalsQueryHandler( IGenericRepository goalRepository, - IPayGateService payGate, IUserDateService userDateService, IGoalProgressReadSyncer goalProgressReadSyncer) : IRequestHandler>> { public async Task>> Handle(GetGoalsQuery request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck.PropagateError>(); - var userToday = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); var freshProgressValues = await goalProgressReadSyncer.ComputeFreshValuesAsync(request.UserId, userToday, cancellationToken); diff --git a/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs b/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs index b3c8e17a..6de8d5ae 100644 --- a/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs @@ -140,13 +140,6 @@ private async Task CheckCreationGatesAsync( return subGateCheck; } - if (request.GoalIds is { Count: > 0 }) - { - var goalLinkGate = await payGate.CanLinkGoalsToHabits(request.UserId, cancellationToken); - if (goalLinkGate.IsFailure) - return goalLinkGate; - } - if (opts.SlipAlertEnabled) { var slipAlertGate = await payGate.CanUseSlipAlerts(request.UserId, cancellationToken); diff --git a/src/Orbit.Application/Habits/Commands/LinkGoalsToHabitCommand.cs b/src/Orbit.Application/Habits/Commands/LinkGoalsToHabitCommand.cs index 6daf8ec8..9c8a161d 100644 --- a/src/Orbit.Application/Habits/Commands/LinkGoalsToHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/LinkGoalsToHabitCommand.cs @@ -16,16 +16,11 @@ public record LinkGoalsToHabitCommand( public class LinkGoalsToHabitCommandHandler( IGenericRepository habitRepository, IGenericRepository goalRepository, - IPayGateService payGate, IGoalCompletionService goalCompletionService, IUserDateService userDateService) : IRequestHandler { public async Task Handle(LinkGoalsToHabitCommand request, CancellationToken cancellationToken) { - var gateCheck = await payGate.CanLinkGoalsToHabits(request.UserId, cancellationToken); - if (gateCheck.IsFailure) - return gateCheck; - var habit = await habitRepository.FindOneTrackedAsync( h => h.Id == request.HabitId && h.UserId == request.UserId, q => q.Include(h => h.Goals), diff --git a/src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs b/src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs index 8d1eec1a..8ac25770 100644 --- a/src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs @@ -37,13 +37,6 @@ public class UpdateHabitCommandHandler( { public async Task Handle(UpdateHabitCommand request, CancellationToken cancellationToken) { - if (request.GoalIds is not null) - { - var goalLinkGate = await payGate.CanLinkGoalsToHabits(request.UserId, cancellationToken); - if (goalLinkGate.IsFailure) - return goalLinkGate; - } - if (request.Options?.SlipAlertEnabled is not null) { var slipAlertGate = await payGate.CanUseSlipAlerts(request.UserId, cancellationToken); diff --git a/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs b/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs index b08df146..9cac2d80 100644 --- a/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs +++ b/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs @@ -53,7 +53,7 @@ public record ApplyOnboardingCommand( /// /// Applies the buffer of answers a user built during pre-auth onboarding in a single transaction: /// creates the habits (trimmed to the free-plan allowance), an optional first log, an optional -/// Pro-gated goal, week-start/color preferences, and flips HasCompletedOnboarding. Idempotent +/// goal, week-start/color preferences, and flips HasCompletedOnboarding. Idempotent /// by construction — an already-onboarded user is a no-op (Applied:false) — so the client can /// flush unconditionally after any successful auth and retry safely under the concurrency pipeline. /// @@ -65,7 +65,6 @@ public record ApplyOnboardingRepositories( public class ApplyOnboardingCommandHandler( ApplyOnboardingRepositories repos, - IPayGateService payGate, IUserDateService userDateService, IAppConfigService appConfig, IUnitOfWork unitOfWork, @@ -121,7 +120,7 @@ private async Task> ApplyOnboardingTransactionAs loggedFirstHabit = true; } - var goalResult = await CreateGoalIfAllowedAsync(request.UserId, request.Goal, today, ct); + var goalResult = await CreateGoalAsync(request.UserId, request.Goal, today, ct); if (goalResult.IsFailure) return goalResult.PropagateError(); var createdGoal = goalResult.Value; @@ -189,16 +188,12 @@ private async Task>> CreateHabitsAsync( return Result.Success(createdHabits); } - private async Task> CreateGoalIfAllowedAsync( + private async Task> CreateGoalAsync( Guid userId, ApplyGoalInput? goalInput, DateOnly today, CancellationToken cancellationToken) { if (goalInput is null) return Result.Success(false); - var goalGate = await payGate.CanAccessGoals(userId, cancellationToken); - if (goalGate.IsFailure) - return Result.Success(false); - if (goalInput.Deadline is { } deadline && deadline < today) return Result.Failure(ErrorMessages.DeadlineInPast); diff --git a/src/Orbit.Domain/Interfaces/IPayGateService.cs b/src/Orbit.Domain/Interfaces/IPayGateService.cs index c16405b2..7ae874cf 100644 --- a/src/Orbit.Domain/Interfaces/IPayGateService.cs +++ b/src/Orbit.Domain/Interfaces/IPayGateService.cs @@ -43,14 +43,9 @@ Task TryConsumeAiMessage( Task CanUseRetrospective(Guid userId, CancellationToken ct = default); /// - /// Checks if the user can access goals (Pro-only feature). + /// Checks if the user can use AI goal reviews (Pro-only feature). /// - Task CanAccessGoals(Guid userId, CancellationToken ct = default); - - /// - /// Checks if the user can create goals (Pro-only feature). - /// - Task CanCreateGoals(Guid userId, CancellationToken ct = default); + Task CanUseGoalReview(Guid userId, CancellationToken ct = default); /// /// Checks if the user can read calendar integration data (Pro-only feature). @@ -117,8 +112,4 @@ Task TryConsumeAiMessage( /// Task CanUseSlipAlerts(Guid userId, CancellationToken ct = default); - /// - /// Checks if the user can manage goal links on habits (Pro-only feature). - /// - Task CanLinkGoalsToHabits(Guid userId, CancellationToken ct = default); } diff --git a/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.Designer.cs b/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.Designer.cs new file mode 100644 index 00000000..4e77bfb6 --- /dev/null +++ b/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.Designer.cs @@ -0,0 +1,2643 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Orbit.Infrastructure.Persistence; + +#nullable disable + +namespace Orbit.Infrastructure.Migrations +{ + [DbContext(typeof(OrbitDbContext))] + [Migration("20260823203458_RemoveGoalsProGate")] + partial class RemoveGoalsProGate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("HabitGoals", b => + { + b.Property("GoalId") + .HasColumnType("uuid"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.HasKey("GoalId", "HabitId"); + + b.HasIndex("HabitId"); + + b.ToTable("HabitGoals"); + }); + + modelBuilder.Entity("HabitTags", b => + { + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.HasKey("HabitId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("HabitTags"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityCheckIn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Note") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PairId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PairId", "CreatedAtUtc"); + + b.HasIndex("PairId", "UserId", "Date") + .IsUnique(); + + b.ToTable("AccountabilityCheckIns"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityPair", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("AddresseeId") + .HasColumnType("uuid"); + + b.Property("Cadence") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("EndedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("AddresseeId"); + + b.HasIndex("RequesterId"); + + b.ToTable("AccountabilityPairs"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityPairHabit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("PairId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HabitId"); + + b.HasIndex("PairId", "UserId", "HabitId") + .IsUnique(); + + b.ToTable("AccountabilityPairHabits"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AgentAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthMethod") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CapabilityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CorrelationId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("OutcomeStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("PolicyDecision") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RedactedArguments") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RiskClass") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ShadowPolicyDecision") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ShadowReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SourceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Summary") + .HasColumnType("text"); + + b.Property("Surface") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TargetId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TargetName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CapabilityId", "CreatedAtUtc"); + + b.HasIndex("UserId", "CreatedAtUtc"); + + b.ToTable("AgentAuditLogs"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AgentStepUpChallengeState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("PendingOperationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifiedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "PendingOperationId", "CreatedAtUtc"); + + b.ToTable("AgentStepUpChallenges"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AiFactExtractionBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("InputFileId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OutputFileId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BatchId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("AiFactExtractionBatches"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AiUsageDaily", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CachedTokens") + .HasColumnType("bigint"); + + b.Property("Calls") + .HasColumnType("bigint"); + + b.Property("CompletionTokens") + .HasColumnType("bigint"); + + b.Property("CostUsd") + .HasColumnType("numeric"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PromptTokens") + .HasColumnType("bigint"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalTokens") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "Model", "Purpose", "UserId") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Date", "Model", "Purpose", "UserId"), false); + + b.ToTable("AiUsageDaily"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsReadOnly") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyPrefix") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)"); + + b.Property("LastUsedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Scopes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("KeyPrefix"); + + b.HasIndex("UserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AppConfig", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Key"); + + b.ToTable("AppConfigs"); + + b.HasData( + new + { + Key = "MaxUserFacts", + Description = "Maximum number of facts the AI can remember per user", + Value = "50" + }, + new + { + Key = "MaxHabitDepth", + Description = "Maximum nesting depth for sub-habits", + Value = "5" + }, + new + { + Key = "MaxTagsPerHabit", + Description = "Maximum number of tags per habit", + Value = "5" + }, + new + { + Key = "ReferralRewardDays", + Description = "Days of Pro added per successful referral", + Value = "10" + }, + new + { + Key = "MaxReferrals", + Description = "Maximum successful referrals per user", + Value = "10" + }, + new + { + Key = "MinSupportedVersion", + Description = "Minimum supported client app version; clients below this receive HTTP 426", + Value = "0.0.0" + }, + new + { + Key = "RequireApiKeyCreationStepUp", + Description = "Turn on once a client build carrying the API key creation challenge flow is live in the Play fleet", + Value = "false" + }); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AppFeatureFlag", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("PlanRequirement") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Key"); + + b.ToTable("AppFeatureFlags"); + + b.HasData( + new + { + Key = "offline_mode", + Description = "Enable offline mode with background sync", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "ai_chat", + Description = "AI chat assistant", + Enabled = true, + PlanRequirement = "Free", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "ai_summary", + Description = "AI daily summary", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "ai_retrospective", + Description = "AI retrospective analysis", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "sub_habits", + Description = "Sub-habit nesting", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "goal_tracking", + Description = "Goal tracking with progress", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "push_notifications", + Description = "Push notification reminders", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "scheduled_reminders", + Description = "Custom scheduled reminders", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "slip_alerts", + Description = "Slip detection alerts", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "checklist_templates", + Description = "Reusable checklist templates", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "habit_duplication", + Description = "Duplicate habits", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "bulk_operations", + Description = "Bulk create/delete/log habits", + Enabled = true, + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "calendar_integration", + Description = "Google Calendar integration", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Key = "api_keys", + Description = "Personal API keys", + Enabled = true, + PlanRequirement = "Pro", + UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.BlockedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlockedId") + .HasColumnType("uuid"); + + b.Property("BlockerId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("BlockedId"); + + b.HasIndex("BlockerId", "BlockedId") + .IsUnique(); + + b.ToTable("BlockedUsers"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Challenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("JoinCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("PeriodEndUtc") + .HasColumnType("date"); + + b.Property("PeriodStartUtc") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("JoinCode") + .IsUnique(); + + b.ToTable("Challenges"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChallengeParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChallengeId") + .HasColumnType("uuid"); + + b.Property("JoinedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("LeftAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ChallengeId", "UserId") + .IsUnique() + .HasFilter("\"LeftAtUtc\" IS NULL"); + + b.ToTable("ChallengeParticipants"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChallengeParticipantHabit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChallengeParticipantId") + .HasColumnType("uuid"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HabitId"); + + b.HasIndex("ChallengeParticipantId", "HabitId") + .IsUnique(); + + b.ToTable("ChallengeParticipantHabits"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Items") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsDeleted"); + + b.HasIndex("UserId", "UpdatedAtUtc"); + + b.ToTable("ChecklistTemplates"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Cheer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HabitId"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId", "CreatedAtUtc"); + + b.ToTable("Cheers"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ClosedMonthRecap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DateFrom") + .HasColumnType("date"); + + b.Property("DateTo") + .HasColumnType("date"); + + b.Property("ResponseJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "DateFrom", "DateTo") + .IsUnique(); + + b.ToTable("ClosedMonthRecaps"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ContentBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Locale") + .IsUnique(); + + b.ToTable("ContentBlocks"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.DistributedRateLimitBucket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("PartitionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WindowEndsAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WindowStartUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PolicyName", "PartitionKey", "WindowStartUtc") + .IsUnique(); + + b.ToTable("DistributedRateLimitBuckets"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.FriendFeedEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AchievementId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ActorUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId", "AchievementId") + .IsUnique() + .HasFilter("\"AchievementId\" IS NOT NULL"); + + b.HasIndex("ActorUserId", "CreatedAtUtc", "Id") + .IsDescending(false, true, true); + + b.HasIndex("ActorUserId", "Type", "Value") + .IsUnique() + .HasFilter("\"AchievementId\" IS NULL"); + + b.ToTable("FriendFeedEvents"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Friendship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddresseeId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("RespondedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("AddresseeId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Friendships"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Goal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentValue") + .HasColumnType("numeric"); + + b.Property("Deadline") + .HasColumnType("date"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("FirstCompletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StreakSyncedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetValue") + .HasColumnType("numeric"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsDeleted"); + + b.HasIndex("UserId", "UpdatedAtUtc"); + + b.ToTable("Goals"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.GoalProgressLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("GoalId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PreviousValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoalId"); + + b.HasIndex("GoalId", "IsDeleted"); + + b.HasIndex("GoalId", "UpdatedAtUtc"); + + b.ToTable("GoalProgressLogs"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.GoogleCalendarSyncSuggestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscoveredAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DismissedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("GoogleEventId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ImportedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ImportedHabitId") + .HasColumnType("uuid"); + + b.Property("RawEventJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDateUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "GoogleEventId") + .IsUnique(); + + b.HasIndex("UserId", "DismissedAtUtc", "ImportedAtUtc"); + + b.ToTable("GoogleCalendarSyncSuggestions"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Habit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChecklistItems") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Days") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DueDate") + .HasColumnType("date"); + + b.Property("DueEndTime") + .HasColumnType("time without time zone"); + + b.Property("DueTime") + .HasColumnType("time without time zone"); + + b.Property("Emoji") + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("FrequencyQuantity") + .HasColumnType("integer"); + + b.Property("FrequencyUnit") + .HasColumnType("integer"); + + b.Property("GoogleEventId") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IsBadHabit") + .HasColumnType("boolean"); + + b.Property("IsCompleted") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFlexible") + .HasColumnType("boolean"); + + b.Property("IsGeneral") + .HasColumnType("boolean"); + + b.Property("OriginalDayOfMonth") + .HasColumnType("integer"); + + b.Property("ParentHabitId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("ReminderEnabled") + .HasColumnType("boolean"); + + b.Property("ReminderTimes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[15]'::jsonb"); + + b.Property("ScheduledReminders") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ScheduledStartDate") + .HasColumnType("date"); + + b.Property("SlipAlertEnabled") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ParentHabitId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "GoogleEventId") + .IsUnique() + .HasFilter("\"GoogleEventId\" IS NOT NULL AND \"IsDeleted\" = FALSE"); + + b.HasIndex("UserId", "IsDeleted"); + + b.HasIndex("UserId", "UpdatedAtUtc"); + + b.ToTable("Habits"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.HabitLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("HabitId", "UpdatedAtUtc"); + + b.HasIndex(new[] { "HabitId", "Date" }, "IX_HabitLogs_HabitId_Date"); + + b.HasIndex(new[] { "HabitId", "Date" }, "IX_HabitLogs_HabitId_Date_Completed") + .IsUnique() + .HasFilter("\"Value\" > 0 AND NOT \"IsDeleted\""); + + b.ToTable("HabitLogs"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DedupeKey") + .HasColumnType("text"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DedupeKey") + .IsUnique() + .HasFilter("\"DedupeKey\" IS NOT NULL"); + + b.HasIndex("Url") + .HasFilter("\"Url\" IS NOT NULL"); + + b.HasIndex("UserId", "CreatedAtUtc") + .IsDescending(false, true); + + b.HasIndex("UserId", "IsDeleted"); + + b.HasIndex("UserId", "IsRead"); + + b.HasIndex("UserId", "UpdatedAtUtc"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.PendingAgentOperationState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ArgumentsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CapabilityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConfirmationRequirement") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ConfirmationTokenHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConfirmedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("OperationFingerprint") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OperationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RiskClass") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("StepUpSatisfiedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Surface") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "CapabilityId"); + + b.HasIndex("UserId", "OperationFingerprint"); + + b.ToTable("PendingAgentOperations"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.PendingClarification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("MissingArgumentKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PartialArgumentsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("QuickActionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ResolvedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ToolName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAtUtc"); + + b.HasIndex("UserId", "CreatedAtUtc"); + + b.ToTable("PendingClarifications"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ProcessedPlayNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProcessedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("ProcessedPlayNotifications"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ProcessedRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ResponseBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAtUtc"); + + b.HasIndex("UserId", "IdempotencyKey", "RequestType") + .IsUnique(); + + b.ToTable("ProcessedRequests"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ProcessedStripeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProcessedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("EventId") + .IsUnique(); + + b.ToTable("ProcessedStripeEvents"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Referral", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferredUserId") + .HasColumnType("uuid"); + + b.Property("ReferrerId") + .HasColumnType("uuid"); + + b.Property("RewardGrantedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ReferredUserId") + .IsUnique(); + + b.HasIndex("ReferrerId"); + + b.ToTable("Referrals"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheerId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ReportedUserId") + .HasColumnType("uuid"); + + b.Property("ReporterId") + .HasColumnType("uuid"); + + b.Property("ReviewedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("CheerId"); + + b.HasIndex("ReportedUserId"); + + b.HasIndex("ReporterId"); + + b.HasIndex("Status"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.SentProactiveCheckin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("SentAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("SentProactiveCheckins"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.SentReminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("MinutesBefore") + .HasColumnType("integer"); + + b.Property("ReminderTimeUtc") + .HasColumnType("time without time zone"); + + b.Property("SentAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("When") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("HabitId", "Date", "MinutesBefore", "ReminderTimeUtc", "When") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("HabitId", "Date", "MinutesBefore", "ReminderTimeUtc", "When"), false); + + b.ToTable("SentReminders"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.SentSlipAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("HabitId") + .HasColumnType("uuid"); + + b.Property("SentAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WeekStart") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.HasIndex("HabitId", "WeekStart") + .IsUnique(); + + b.ToTable("SentSlipAlerts"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.StreakFreeze", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UsedOnDate") + .HasColumnType("date"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "UsedOnDate") + .IsUnique(); + + b.ToTable("StreakFreezes"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsDeleted"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasFilter("\"IsDeleted\" = FALSE"); + + b.HasIndex("UserId", "UpdatedAtUtc"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdRewardBonusMessages") + .HasColumnType("integer"); + + b.Property("AdRewardsClaimedToday") + .HasColumnType("integer"); + + b.Property("AiMemoryEnabled") + .HasColumnType("boolean"); + + b.Property("AiMessagesResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AiMessagesUsedThisMonth") + .HasColumnType("integer"); + + b.Property("AiSummaryEnabled") + .HasColumnType("boolean"); + + b.Property("ColorScheme") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentStreak") + .HasColumnType("integer"); + + b.Property("DeactivatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("GoogleAccessToken") + .HasColumnType("text"); + + b.Property("GoogleCalendarAutoSyncEnabled") + .HasColumnType("boolean"); + + b.Property("GoogleCalendarAutoSyncStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("GoogleCalendarLastSyncError") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GoogleCalendarLastSyncedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GoogleCalendarSelectedIds") + .HasColumnType("text"); + + b.Property("GoogleCalendarSyncReconciledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GoogleRefreshToken") + .HasColumnType("text"); + + b.Property("Handle") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HasCompletedOnboarding") + .HasColumnType("boolean"); + + b.Property("HasCompletedOnboardingChecklist") + .HasColumnType("boolean"); + + b.Property("HasCompletedTour") + .HasColumnType("boolean"); + + b.Property("HasCreatedFirstHabit") + .HasColumnType("boolean"); + + b.Property("HasImportedCalendar") + .HasColumnType("boolean"); + + b.Property("HasLoggedFirstHabit") + .HasColumnType("boolean"); + + b.Property("HasSeenImportPrompt") + .HasColumnType("boolean"); + + b.Property("HasTriedAstra") + .HasColumnType("boolean"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("IsDeactivated") + .HasColumnType("boolean"); + + b.Property("IsLifetimePro") + .HasColumnType("boolean"); + + b.Property("Language") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("LastActiveDate") + .HasColumnType("date"); + + b.Property("LastAdRewardAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastAdRewardLocalDate") + .HasColumnType("date"); + + b.Property("LastFreezeAwardStreak") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("LongestStreak") + .HasColumnType("integer"); + + b.Property("MarketingConsentUpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("MarketingEmailConsent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Plan") + .HasColumnType("integer"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayPurchaseToken") + .HasColumnType("text"); + + b.Property("ProactiveAstraEnabled") + .HasColumnType("boolean"); + + b.Property("PublicProfileShowAchievements") + .HasColumnType("boolean"); + + b.Property("PublicProfileShowLevel") + .HasColumnType("boolean"); + + b.Property("PublicProfileShowStreak") + .HasColumnType("boolean"); + + b.Property("PublicProfileShowTopHabits") + .HasColumnType("boolean"); + + b.Property("PublicProfileSlug") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ReferralCode") + .HasColumnType("text"); + + b.Property("ReferralCouponId") + .HasColumnType("text"); + + b.Property("ReferredByUserId") + .HasColumnType("uuid"); + + b.Property("ScheduledDeletionAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SocialOptIn") + .HasColumnType("boolean"); + + b.Property("StreakFreezesAccumulated") + .HasColumnType("integer"); + + b.Property("StripeCustomerId") + .HasColumnType("text"); + + b.Property("StripeSubscriptionEventCreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("StripeSubscriptionId") + .HasColumnType("text"); + + b.Property("SubscriptionEndedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriptionInterval") + .HasColumnType("integer"); + + b.Property("SubscriptionLapseReason") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SubscriptionSource") + .HasColumnType("integer"); + + b.Property("ThemePreference") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("TimeZone") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TotalXp") + .HasColumnType("integer"); + + b.Property("TrialEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeekStartDay") + .HasColumnType("integer"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("PlayPurchaseToken") + .IsUnique() + .HasFilter("\"PlayPurchaseToken\" IS NOT NULL"); + + b.HasIndex("PublicProfileSlug") + .IsUnique() + .HasFilter("\"PublicProfileSlug\" IS NOT NULL"); + + b.HasIndex("ReferralCode") + .IsUnique() + .HasFilter("\"ReferralCode\" IS NOT NULL"); + + b.HasIndex("GoogleCalendarAutoSyncEnabled", "GoogleCalendarLastSyncedAt") + .HasFilter("\"GoogleCalendarAutoSyncEnabled\" = TRUE"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.UserAchievement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AchievementId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EarnedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "AchievementId") + .IsUnique(); + + b.ToTable("UserAchievements"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.UserFact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExtractedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("FactText") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsDeleted"); + + b.ToTable("UserFacts"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.XpAwardLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("AwardedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SourceId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "AwardedAtUtc"); + + b.ToTable("XpAwardLogs"); + }); + + modelBuilder.Entity("HabitGoals", b => + { + b.HasOne("Orbit.Domain.Entities.Goal", null) + .WithMany() + .HasForeignKey("GoalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany() + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HabitTags", b => + { + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany() + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityCheckIn", b => + { + b.HasOne("Orbit.Domain.Entities.AccountabilityPair", null) + .WithMany() + .HasForeignKey("PairId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityPair", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("AddresseeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AccountabilityPairHabit", b => + { + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany() + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.AccountabilityPair", null) + .WithMany() + .HasForeignKey("PairId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.AiFactExtractionBatch", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ApiKey", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.BlockedUser", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("BlockedId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("BlockerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Challenge", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChallengeParticipant", b => + { + b.HasOne("Orbit.Domain.Entities.Challenge", null) + .WithMany("Participants") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChallengeParticipantHabit", b => + { + b.HasOne("Orbit.Domain.Entities.ChallengeParticipant", null) + .WithMany("LinkedHabits") + .HasForeignKey("ChallengeParticipantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany() + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChecklistTemplate", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Cheer", b => + { + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany() + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ClosedMonthRecap", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.FriendFeedEvent", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Friendship", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("AddresseeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.GoalProgressLog", b => + { + b.HasOne("Orbit.Domain.Entities.Goal", null) + .WithMany("ProgressLogs") + .HasForeignKey("GoalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.GoogleCalendarSyncSuggestion", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Habit", b => + { + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany("Children") + .HasForeignKey("ParentHabitId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.HabitLog", b => + { + b.HasOne("Orbit.Domain.Entities.Habit", null) + .WithMany("Logs") + .HasForeignKey("HabitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.PendingClarification", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ProcessedRequest", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.PushSubscription", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Report", b => + { + b.HasOne("Orbit.Domain.Entities.Cheer", null) + .WithMany() + .HasForeignKey("CheerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("ReportedUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("ReporterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.SentProactiveCheckin", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.StreakFreeze", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Tag", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.UserSession", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.XpAwardLog", b => + { + b.HasOne("Orbit.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Challenge", b => + { + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.ChallengeParticipant", b => + { + b.Navigation("LinkedHabits"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Goal", b => + { + b.Navigation("ProgressLogs"); + }); + + modelBuilder.Entity("Orbit.Domain.Entities.Habit", b => + { + b.Navigation("Children"); + + b.Navigation("Logs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.cs b/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.cs new file mode 100644 index 00000000..866b0a39 --- /dev/null +++ b/src/Orbit.Infrastructure/Migrations/20260823203458_RemoveGoalsProGate.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Orbit.Infrastructure.Migrations +{ + /// + public partial class RemoveGoalsProGate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AppConfigs", + keyColumn: "Key", + keyValue: "GoalsProOnly"); + + migrationBuilder.UpdateData( + table: "AppFeatureFlags", + keyColumn: "Key", + keyValue: "goal_tracking", + column: "PlanRequirement", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "AppFeatureFlags", + keyColumn: "Key", + keyValue: "goal_tracking", + column: "PlanRequirement", + value: "Pro"); + + migrationBuilder.InsertData( + table: "AppConfigs", + columns: new[] { "Key", "Description", "Value" }, + values: new object[] { "GoalsProOnly", "Whether goal access is restricted to Pro users", "true" }); + } + } +} diff --git a/src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs b/src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs index 5d8ab169..f18cf428 100644 --- a/src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs +++ b/src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs @@ -557,7 +557,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) Key = "goal_tracking", Description = "Goal tracking with progress", Enabled = true, - PlanRequirement = "Pro", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, 0, DateTimeKind.Utc) }, new diff --git a/src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs b/src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs index 76c80f6e..95fa3523 100644 --- a/src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs +++ b/src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs @@ -503,7 +503,7 @@ private static void ConfigureAppFeatureFlagEntity(ModelBuilder modelBuilder) new { Key = "ai_summary", Enabled = true, PlanRequirement = (string?)"Pro", Description = (string?)"AI daily summary", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, new { Key = "ai_retrospective", Enabled = true, PlanRequirement = (string?)"Pro", Description = (string?)"AI retrospective analysis", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, new { Key = "sub_habits", Enabled = true, PlanRequirement = (string?)"Pro", Description = (string?)"Sub-habit nesting", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, - new { Key = "goal_tracking", Enabled = true, PlanRequirement = (string?)"Pro", Description = (string?)"Goal tracking with progress", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, + new { Key = "goal_tracking", Enabled = true, PlanRequirement = (string?)null, Description = (string?)"Goal tracking with progress", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, new { Key = "push_notifications", Enabled = true, PlanRequirement = (string?)null, Description = (string?)"Push notification reminders", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, new { Key = "scheduled_reminders", Enabled = true, PlanRequirement = (string?)null, Description = (string?)"Custom scheduled reminders", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, new { Key = "slip_alerts", Enabled = true, PlanRequirement = (string?)"Pro", Description = (string?)"Slip detection alerts", UpdatedAtUtc = new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc) }, diff --git a/src/Orbit.Infrastructure/Services/AgentCatalogService.Capabilities.cs b/src/Orbit.Infrastructure/Services/AgentCatalogService.Capabilities.cs index e2dde36e..0b19dcdf 100644 --- a/src/Orbit.Infrastructure/Services/AgentCatalogService.Capabilities.cs +++ b/src/Orbit.Infrastructure/Services/AgentCatalogService.Capabilities.cs @@ -319,8 +319,6 @@ private static AgentCapability[] GoalCapabilities() isMutation: false, isPhaseOneReadOnly: false, AgentConfirmationRequirement.None, - planRequirement: "Pro", - featureFlagKeys: ["goal_tracking"], chatTools: ["query_goals", "review_goals"], mcpTools: ["list_goals", "get_goal", "get_goal_metrics", "get_goal_review"], controllerActions: @@ -343,8 +341,6 @@ private static AgentCapability[] GoalCapabilities() isMutation: true, isPhaseOneReadOnly: false, AgentConfirmationRequirement.None, - planRequirement: "Pro", - featureFlagKeys: ["goal_tracking"], chatTools: ["create_goal", "update_goal", "update_goal_status", "update_goal_progress", "link_habits_to_goal", "reorder_goals"], mcpTools: ["create_goal", "update_goal", "update_goal_progress", "update_goal_status", "reorder_goals", "link_habits_to_goal"], controllerActions: @@ -368,8 +364,6 @@ private static AgentCapability[] GoalCapabilities() isMutation: true, isPhaseOneReadOnly: false, AgentConfirmationRequirement.FreshConfirmation, - planRequirement: "Pro", - featureFlagKeys: ["goal_tracking"], chatTools: ["delete_goal"], mcpTools: ["delete_goal"], controllerActions: ["GoalsController.DeleteGoal"]) diff --git a/tests/Orbit.Application.Tests/Caching/GoalAiCacheInvalidationTests.cs b/tests/Orbit.Application.Tests/Caching/GoalAiCacheInvalidationTests.cs index d30bc9d0..65ea3d3c 100644 --- a/tests/Orbit.Application.Tests/Caching/GoalAiCacheInvalidationTests.cs +++ b/tests/Orbit.Application.Tests/Caching/GoalAiCacheInvalidationTests.cs @@ -49,17 +49,15 @@ public async Task CreateGoal_InvalidatesCachedGoalReview() var goalRepo = Substitute.For>(); var habitRepo = Substitute.For>(); - var payGate = Substitute.For(); var userDateService = Substitute.For(); var gamificationService = Substitute.For(); var unitOfWork = Substitute.For(); - payGate.CanAccessGoals(Arg.Any(), Arg.Any()).Returns(Result.Success()); userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()) .Returns(new DateOnly(2026, 7, 12)); var handler = new CreateGoalCommandHandler( - goalRepo, habitRepo, payGate, userDateService, gamificationService, + goalRepo, habitRepo, userDateService, gamificationService, Substitute.For(), unitOfWork, cache, Substitute.For>()); diff --git a/tests/Orbit.Application.Tests/Commands/Goals/CreateGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/CreateGoalCommandHandlerTests.cs index 7f7841ff..c4e3cb48 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/CreateGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/CreateGoalCommandHandlerTests.cs @@ -21,7 +21,6 @@ public class CreateGoalCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); private readonly IGenericRepository _habitRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly IGamificationService _gamificationService = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); @@ -35,12 +34,10 @@ public class CreateGoalCommandHandlerTests public CreateGoalCommandHandlerTests() { _handler = new CreateGoalCommandHandler( - _goalRepo, _habitRepo, _payGate, _userDateService, _gamificationService, + _goalRepo, _habitRepo, _userDateService, _gamificationService, _goalCompletionService, _unitOfWork, _cache, Substitute.For>()); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()) .Returns(Today); } @@ -74,21 +71,6 @@ await _goalRepo.Received(1).AddAsync( Arg.Any()); } - [Fact] - public async Task Handle_PayGateLimitReached_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new CreateGoalCommand(UserId, "New goal", null, 10, "units", null); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be("PAY_GATE"); - await _goalRepo.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); - } - [Fact] public async Task Handle_EmptyTitle_ReturnsFailure() { diff --git a/tests/Orbit.Application.Tests/Commands/Goals/DeleteGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/DeleteGoalCommandHandlerTests.cs index 56925a9b..3bde9288 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/DeleteGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/DeleteGoalCommandHandlerTests.cs @@ -13,7 +13,6 @@ namespace Orbit.Application.Tests.Commands.Goals; public class DeleteGoalCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); private readonly IUserDateService _userDateService = Substitute.For(); @@ -25,10 +24,8 @@ public class DeleteGoalCommandHandlerTests public DeleteGoalCommandHandlerTests() { - _handler = new DeleteGoalCommandHandler(_goalRepo, _payGate, _unitOfWork, _userDateService, _cache); + _handler = new DeleteGoalCommandHandler(_goalRepo, _unitOfWork, _userDateService, _cache); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } [Fact] @@ -88,18 +85,4 @@ public async Task Handle_WrongUser_GoalNotFoundBecauseFilterExcludesIt() result.ErrorCode.Should().Be(ErrorCodes.GoalNotFound); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new DeleteGoalCommand(UserId, GoalId); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } } diff --git a/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs index b1c18d07..2bb2efe4 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs @@ -17,7 +17,6 @@ public class LinkHabitsToGoalCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); private readonly IGenericRepository _habitRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); private readonly IUserDateService _userDateService = Substitute.For(); @@ -30,11 +29,9 @@ public class LinkHabitsToGoalCommandHandlerTests public LinkHabitsToGoalCommandHandlerTests() { _handler = new LinkHabitsToGoalCommandHandler( - _goalRepo, _habitRepo, _payGate, _goalCompletionService, + _goalRepo, _habitRepo, _goalCompletionService, _userDateService, _cache); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } [Fact] @@ -155,23 +152,6 @@ public async Task Handle_ReplacesExistingLinks() goal.Habits.Should().NotContain(oldHabit); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new LinkHabitsToGoalCommand(UserId, GoalId, new List { Guid.NewGuid() }); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _goalCompletionService.DidNotReceive().SyncDerivedGoalsAsync( - Arg.Any(), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any()); - } - [Fact] public async Task Handle_ForeignOrMissingHabitId_ReturnsFailureWithoutClearing() { diff --git a/tests/Orbit.Application.Tests/Commands/Goals/ReorderGoalsCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/ReorderGoalsCommandHandlerTests.cs index e90773a9..71817df3 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/ReorderGoalsCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/ReorderGoalsCommandHandlerTests.cs @@ -13,7 +13,6 @@ namespace Orbit.Application.Tests.Commands.Goals; public class ReorderGoalsCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); private readonly IUserDateService _userDateService = Substitute.For(); @@ -24,10 +23,8 @@ public class ReorderGoalsCommandHandlerTests public ReorderGoalsCommandHandlerTests() { - _handler = new ReorderGoalsCommandHandler(_goalRepo, _payGate, _unitOfWork, _userDateService, _cache); + _handler = new ReorderGoalsCommandHandler(_goalRepo, _unitOfWork, _userDateService, _cache); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } private void SetupGoalsForUser(params Goal[] goals) @@ -124,21 +121,6 @@ await _goalRepo.DidNotReceive().FindOneTrackedAsync( Arg.Any()); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new ReorderGoalsCommand(UserId, new List()); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } - [Fact] public async Task Handle_DuplicatePositions_NormalizesToContiguousSequence() { diff --git a/tests/Orbit.Application.Tests/Commands/Goals/RestoreGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/RestoreGoalCommandHandlerTests.cs index 2a6ad062..320772f3 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/RestoreGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/RestoreGoalCommandHandlerTests.cs @@ -13,7 +13,6 @@ namespace Orbit.Application.Tests.Commands.Goals; public class RestoreGoalCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); private readonly IUserDateService _userDateService = Substitute.For(); @@ -25,10 +24,8 @@ public class RestoreGoalCommandHandlerTests public RestoreGoalCommandHandlerTests() { - _handler = new RestoreGoalCommandHandler(_goalRepo, _payGate, _unitOfWork, _userDateService, _cache); + _handler = new RestoreGoalCommandHandler(_goalRepo, _unitOfWork, _userDateService, _cache); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } private void SetupGoals(params Goal[] goals) @@ -76,16 +73,4 @@ public async Task Handle_GoalNotFound_ReturnsFailure() result.ErrorCode.Should().Be(ErrorCodes.GoalNotFound); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var result = await _handler.Handle(new RestoreGoalCommand(UserId, GoalId), CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } } diff --git a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalCommandHandlerTests.cs index 23e07ccd..83f03b39 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalCommandHandlerTests.cs @@ -18,7 +18,6 @@ public class UpdateGoalCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); private readonly IGenericRepository _progressLogRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); @@ -32,10 +31,8 @@ public class UpdateGoalCommandHandlerTests public UpdateGoalCommandHandlerTests() { _handler = new UpdateGoalCommandHandler( - new GoalRepositories(_goalRepo, _progressLogRepo), _payGate, _userDateService, + new GoalRepositories(_goalRepo, _progressLogRepo), _userDateService, _goalCompletionService, _unitOfWork, _cache); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()) .Returns(Today); } @@ -145,21 +142,6 @@ public async Task Handle_WithDeadline_UpdatesDeadline() goal.Deadline.Should().Be(deadline); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new UpdateGoalCommand(UserId, GoalId, "Title", null, 100, "km", null); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } - [Fact] public async Task Handle_DeadlineInPast_ReturnsFailureAndDoesNotLoadGoal() { diff --git a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalProgressCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalProgressCommandHandlerTests.cs index e4bef4df..5f23adbd 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalProgressCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalProgressCommandHandlerTests.cs @@ -16,7 +16,6 @@ public class UpdateGoalProgressCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); private readonly IGenericRepository _progressLogRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); @@ -30,7 +29,7 @@ public class UpdateGoalProgressCommandHandlerTests public UpdateGoalProgressCommandHandlerTests() { _handler = new UpdateGoalProgressCommandHandler( - new GoalRepositories(_goalRepo, _progressLogRepo), _payGate, _goalCompletionService, + new GoalRepositories(_goalRepo, _progressLogRepo), _goalCompletionService, _unitOfWork, _userDateService, _cache); _unitOfWork.ExecuteInTransactionAsync( Arg.Any>>>(), @@ -38,8 +37,6 @@ public UpdateGoalProgressCommandHandlerTests() .Returns(call => call.ArgAt>>>(0)( call.ArgAt(1))); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } [Fact] @@ -226,18 +223,4 @@ private void SetupGoalFound(Goal goal) .Returns(goal); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new UpdateGoalProgressCommand(UserId, GoalId, 50); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } } diff --git a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalStatusCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalStatusCommandHandlerTests.cs index 87776f83..1d0a8bb1 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalStatusCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/UpdateGoalStatusCommandHandlerTests.cs @@ -17,7 +17,6 @@ namespace Orbit.Application.Tests.Commands.Goals; public class UpdateGoalStatusCommandHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); @@ -31,10 +30,8 @@ public class UpdateGoalStatusCommandHandlerTests public UpdateGoalStatusCommandHandlerTests() { _handler = new UpdateGoalStatusCommandHandler( - _goalRepo, _payGate, _goalCompletionService, _unitOfWork, _userDateService, _cache); + _goalRepo, _goalCompletionService, _unitOfWork, _userDateService, _cache); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); } [Fact] @@ -187,18 +184,4 @@ private void SetupGoalFound(Goal goal) .Returns(goal); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var command = new UpdateGoalStatusCommand(UserId, GoalId, GoalStatus.Completed); - - var result = await _handler.Handle(command, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } } diff --git a/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs index a8c25525..4839e15a 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs @@ -40,8 +40,6 @@ public CreateHabitCommandHandlerTests() .Returns(Result.Success()); _payGate.CanCreateSubHabits(Arg.Any(), Arg.Any()) .Returns(Result.Success()); - _payGate.CanLinkGoalsToHabits(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); _payGate.CanUseSlipAlerts(Arg.Any(), Arg.Any()) .Returns(Result.Success()); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()) diff --git a/tests/Orbit.Application.Tests/Commands/Habits/LinkGoalsToHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/LinkGoalsToHabitCommandHandlerTests.cs index 0bb2bcb5..22e7d2c6 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/LinkGoalsToHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/LinkGoalsToHabitCommandHandlerTests.cs @@ -15,7 +15,6 @@ public class LinkGoalsToHabitCommandHandlerTests { private readonly IGenericRepository _habitRepo = Substitute.For>(); private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IGoalCompletionService _goalCompletionService = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly LinkGoalsToHabitCommandHandler _handler; @@ -25,11 +24,9 @@ public class LinkGoalsToHabitCommandHandlerTests public LinkGoalsToHabitCommandHandlerTests() { - _payGate.CanLinkGoalsToHabits(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Result.Success())); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); _handler = new LinkGoalsToHabitCommandHandler( - _habitRepo, _goalRepo, _payGate, _goalCompletionService, _userDateService); + _habitRepo, _goalRepo, _goalCompletionService, _userDateService); } private static Habit CreateTestHabit() => diff --git a/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitCommandHandlerTests.cs index 110ad31e..b03c074f 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitCommandHandlerTests.cs @@ -28,8 +28,6 @@ public class UpdateHabitCommandHandlerTests public UpdateHabitCommandHandlerTests() { - _payGate.CanLinkGoalsToHabits(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Result.Success())); _payGate.CanUseSlipAlerts(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(Result.Success())); _handler = new UpdateHabitCommandHandler( diff --git a/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitGoalSyncTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitGoalSyncTests.cs index 5cd238ea..7e51ba6c 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitGoalSyncTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/UpdateHabitGoalSyncTests.cs @@ -31,8 +31,6 @@ public class UpdateHabitGoalSyncTests public UpdateHabitGoalSyncTests() { - _payGate.CanLinkGoalsToHabits(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Result.Success())); _payGate.CanUseSlipAlerts(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(Result.Success())); _handler = new UpdateHabitCommandHandler( diff --git a/tests/Orbit.Application.Tests/Commands/Profile/ApplyOnboardingCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Profile/ApplyOnboardingCommandHandlerTests.cs index 65366388..14a05cc3 100644 --- a/tests/Orbit.Application.Tests/Commands/Profile/ApplyOnboardingCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Profile/ApplyOnboardingCommandHandlerTests.cs @@ -16,7 +16,6 @@ public class ApplyOnboardingCommandHandlerTests private readonly IGenericRepository _userRepo = Substitute.For>(); private readonly IGenericRepository _habitRepo = Substitute.For>(); private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly IAppConfigService _appConfig = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); @@ -28,8 +27,6 @@ public class ApplyOnboardingCommandHandlerTests public ApplyOnboardingCommandHandlerTests() { _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Result.Success())); _appConfig.GetAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(AppConstants.DefaultFreeMaxHabits); _unitOfWork.ExecuteInTransactionAsync( @@ -45,7 +42,7 @@ public ApplyOnboardingCommandHandlerTests() private ApplyOnboardingCommandHandler CreateHandler() => new( new ApplyOnboardingRepositories(_userRepo, _habitRepo, _goalRepo), - _payGate, _userDateService, _appConfig, _unitOfWork, _cache); + _userDateService, _appConfig, _unitOfWork, _cache); private void SetupUser(User user) { @@ -215,12 +212,10 @@ public async Task Apply_FreeUserWithCompletedTasks_CreatesFullRequestedSet() } [Fact] - public async Task Apply_GoalGateFails_SkipsGoalButStillApplies() + public async Task Apply_FreeUserGoal_CreatesGoalAndApplies() { var user = CreateFreeUser(); SetupUser(user); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Result.PayGateFailure("Goals are a Pro feature. Upgrade to unlock!"))); var command = new ApplyOnboardingCommand( UserId, [Habit("Drink water")], null, @@ -230,9 +225,9 @@ public async Task Apply_GoalGateFails_SkipsGoalButStillApplies() result.IsSuccess.Should().BeTrue(); result.Value.Applied.Should().BeTrue(); - result.Value.CreatedGoal.Should().BeFalse(); + result.Value.CreatedGoal.Should().BeTrue(); result.Value.CreatedHabitCount.Should().Be(1); - await _goalRepo.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); + await _goalRepo.Received(1).AddAsync(Arg.Any(), Arg.Any()); user.HasCompletedOnboarding.Should().BeTrue(); } diff --git a/tests/Orbit.Application.Tests/Common/PayGateServiceTests.cs b/tests/Orbit.Application.Tests/Common/PayGateServiceTests.cs index 3d78cd86..fa0d7f07 100644 --- a/tests/Orbit.Application.Tests/Common/PayGateServiceTests.cs +++ b/tests/Orbit.Application.Tests/Common/PayGateServiceTests.cs @@ -29,7 +29,6 @@ public PayGateServiceTests() _appConfig.GetAsync("ProAiMessagesPerMonth", 500, Arg.Any()).Returns(500); _appConfig.GetAsync("DailySummaryProOnly", true, Arg.Any()).Returns(true); _appConfig.GetAsync("RetrospectiveProOnly", true, Arg.Any()).Returns(true); - _appConfig.GetAsync("GoalsProOnly", true, Arg.Any()).Returns(true); } private static User CreateFreeUser() @@ -507,53 +506,41 @@ public async Task CanUseRetrospective_ConfigDisabled_FreeUserAllowed() } [Fact] - public async Task CanCreateGoals_ProUser_Success() + public async Task CanUseGoalReview_ProUser_Success() { var user = CreateProUser(); _userRepo.GetByIdAsync(UserId, Arg.Any()).Returns(user); - var result = await _sut.CanCreateGoals(UserId); + var result = await _sut.CanUseGoalReview(UserId); result.IsSuccess.Should().BeTrue(); } [Fact] - public async Task CanCreateGoals_FreeUser_PayGateFailure() + public async Task CanUseGoalReview_FreeUser_PayGateFailure() { var user = CreateFreeUser(); user.StartTrial(DateTime.UtcNow.AddDays(-1)); _userRepo.GetByIdAsync(UserId, Arg.Any()).Returns(user); - var result = await _sut.CanCreateGoals(UserId); + var result = await _sut.CanUseGoalReview(UserId); result.IsFailure.Should().BeTrue(); result.ErrorCode.Should().Be("PAY_GATE"); + result.Error.Should().Contain("Goal reviews are a Pro feature"); } [Fact] - public async Task CanCreateGoals_UserNotFound_Failure() + public async Task CanUseGoalReview_UserNotFound_Failure() { _userRepo.GetByIdAsync(UserId, Arg.Any()).Returns((User?)null); - var result = await _sut.CanCreateGoals(UserId); + var result = await _sut.CanUseGoalReview(UserId); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("User not found"); } - [Fact] - public async Task CanCreateGoals_ConfigDisabled_FreeUserAllowed() - { - var user = CreateFreeUser(); - user.StartTrial(DateTime.UtcNow.AddDays(-1)); - _userRepo.GetByIdAsync(UserId, Arg.Any()).Returns(user); - _appConfig.GetAsync("GoalsProOnly", true, Arg.Any()).Returns(false); - - var result = await _sut.CanCreateGoals(UserId); - - result.IsSuccess.Should().BeTrue(); - } - [Fact] public async Task CanCreateApiKeys_ProUser_Success() { diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalByIdQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalByIdQueryHandlerTests.cs index c396812c..e655b93f 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalByIdQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalByIdQueryHandlerTests.cs @@ -11,7 +11,6 @@ namespace Orbit.Application.Tests.Queries.Goals; public class GetGoalByIdQueryHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly GetGoalByIdQueryHandler _handler; @@ -21,9 +20,7 @@ public class GetGoalByIdQueryHandlerTests public GetGoalByIdQueryHandlerTests() { - _handler = new GetGoalByIdQueryHandler(_goalRepo, _payGate, _userDateService); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.Success()); + _handler = new GetGoalByIdQueryHandler(_goalRepo, _userDateService); _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()).Returns(Today); } @@ -132,20 +129,6 @@ public async Task Handle_GoalFound_MapsLinkedHabits() result.Value.ProgressHistory.Should().BeEmpty(); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.PayGateFailure("Goals are a Pro feature")); - - var query = new GetGoalByIdQuery(UserId, GoalId); - - var result = await _handler.Handle(query, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Orbit.Domain.Common.Result.PayGateErrorCode); - } - [Fact] public async Task Handle_WithBadHabitLinkedStreakGoal_ReturnsFreshCurrentValueWithoutPersistingCompletion() { diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalDetailQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalDetailQueryHandlerTests.cs index 5fc78f40..b01f0bd8 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalDetailQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalDetailQueryHandlerTests.cs @@ -11,7 +11,6 @@ namespace Orbit.Application.Tests.Queries.Goals; public class GetGoalDetailQueryHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly GetGoalDetailQueryHandler _handler; @@ -21,9 +20,7 @@ public class GetGoalDetailQueryHandlerTests public GetGoalDetailQueryHandlerTests() { - _handler = new GetGoalDetailQueryHandler(_goalRepo, _payGate, _userDateService); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.Success()); + _handler = new GetGoalDetailQueryHandler(_goalRepo, _userDateService); _userDateService.GetUserTodayAsync(UserId, Arg.Any()).Returns(Today); } @@ -102,20 +99,6 @@ public async Task Handle_CallsUserDateService() await _userDateService.Received(1).GetUserTodayAsync(UserId, Arg.Any()); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.PayGateFailure("Goals are a Pro feature")); - - var query = new GetGoalDetailQuery(UserId, GoalId); - - var result = await _handler.Handle(query, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Orbit.Domain.Common.Result.PayGateErrorCode); - } - [Fact] public async Task Handle_WithBadHabitLinkedStreakGoal_ReturnsFreshCurrentValueWithoutPersistingCompletion() { diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalMetricsQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalMetricsQueryHandlerTests.cs index b60a26dd..47d63e4e 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalMetricsQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalMetricsQueryHandlerTests.cs @@ -12,7 +12,6 @@ namespace Orbit.Application.Tests.Queries.Goals; public class GetGoalMetricsQueryHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly IGoalProgressReadSyncer _goalProgressReadSyncer = Substitute.For(); private readonly GetGoalMetricsQueryHandler _handler; @@ -25,11 +24,8 @@ public GetGoalMetricsQueryHandlerTests() { _handler = new GetGoalMetricsQueryHandler( _goalRepo, - _payGate, _userDateService, _goalProgressReadSyncer); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.Success()); _userDateService.GetUserTodayAsync(UserId, Arg.Any()).Returns(Today); _goalProgressReadSyncer.ComputeFreshValuesAsync(UserId, Today, Arg.Any()) .Returns(new Dictionary()); @@ -91,20 +87,6 @@ public async Task Handle_CallsUserDateService() await _userDateService.Received(1).GetUserTodayAsync(UserId, Arg.Any()); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.PayGateFailure("Goals are a Pro feature")); - - var query = new GetGoalMetricsQuery(UserId, GoalId); - - var result = await _handler.Handle(query, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Orbit.Domain.Common.Result.PayGateErrorCode); - } - [Fact] public async Task Handle_WithBadHabitLinkedStreakGoal_CalculatesMetricsFromFreshValueWithoutPersisting() { diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalProgressHistoryQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalProgressHistoryQueryHandlerTests.cs index c544623b..fd3b8767 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalProgressHistoryQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalProgressHistoryQueryHandlerTests.cs @@ -12,7 +12,6 @@ public class GetGoalProgressHistoryQueryHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); private readonly IGenericRepository _progressLogRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly GetGoalProgressHistoryQueryHandler _handler; private static readonly Guid UserId = Guid.NewGuid(); @@ -21,9 +20,7 @@ public class GetGoalProgressHistoryQueryHandlerTests public GetGoalProgressHistoryQueryHandlerTests() { - _handler = new GetGoalProgressHistoryQueryHandler(_goalRepo, _progressLogRepo, _payGate); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); + _handler = new GetGoalProgressHistoryQueryHandler(_goalRepo, _progressLogRepo); _goalRepo.AnyAsync(Arg.Any>>(), Arg.Any()) .Returns(true); } @@ -109,18 +106,4 @@ await _progressLogRepo.DidNotReceive().FindAsync( Arg.Any>>(), Arg.Any()); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); - - var query = new GetGoalProgressHistoryQuery(UserId, GoalId, Today.AddDays(-5), Today); - var result = await _handler.Handle(query, CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Result.PayGateErrorCode); - await _goalRepo.DidNotReceive().AnyAsync( - Arg.Any>>(), Arg.Any()); - } } diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalReviewQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalReviewQueryHandlerTests.cs index a82a0fa9..20b3b77e 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalReviewQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalReviewQueryHandlerTests.cs @@ -46,7 +46,7 @@ private static Goal CreateTestGoal() [Fact] public async Task Handle_GeneratesNewReview_WhenNotCached() { - _payGate.CanAccessGoals(UserId, Arg.Any()).Returns(Result.Success()); + _payGate.CanUseGoalReview(UserId, Arg.Any()).Returns(Result.Success()); var goal = CreateTestGoal(); _goalRepo.FindAsync( @@ -71,7 +71,7 @@ public async Task Handle_GeneratesNewReview_WhenNotCached() [Fact] public async Task Handle_RefreshesStreakGoalValue_BeforeBuildingContext() { - _payGate.CanAccessGoals(UserId, Arg.Any()).Returns(Result.Success()); + _payGate.CanUseGoalReview(UserId, Arg.Any()).Returns(Result.Success()); var streakGoal = Goal.Create(new Goal.CreateGoalParams( UserId, "Avoid doom scrolling", 7, "days", Type: GoalType.Streak)).Value; @@ -105,7 +105,7 @@ public async Task Handle_RefreshesStreakGoalValue_BeforeBuildingContext() [Fact] public async Task Handle_ReturnsCachedReview_WhenCached() { - _payGate.CanAccessGoals(UserId, Arg.Any()).Returns(Result.Success()); + _payGate.CanUseGoalReview(UserId, Arg.Any()).Returns(Result.Success()); var goal = CreateTestGoal(); _goalRepo.FindAsync( @@ -131,8 +131,8 @@ public async Task Handle_ReturnsCachedReview_WhenCached() [Fact] public async Task Handle_PayGateFails_ReturnsFailure() { - _payGate.CanAccessGoals(UserId, Arg.Any()) - .Returns(Result.PayGateFailure("Goals are a Pro feature")); + _payGate.CanUseGoalReview(UserId, Arg.Any()) + .Returns(Result.PayGateFailure("Goal reviews are a Pro feature")); var query = new GetGoalReviewQuery(UserId, "en"); @@ -144,7 +144,7 @@ public async Task Handle_PayGateFails_ReturnsFailure() [Fact] public async Task Handle_NoActiveGoals_ReturnsFailure() { - _payGate.CanAccessGoals(UserId, Arg.Any()).Returns(Result.Success()); + _payGate.CanUseGoalReview(UserId, Arg.Any()).Returns(Result.Success()); _goalRepo.FindAsync( Arg.Any>>(), @@ -163,7 +163,7 @@ public async Task Handle_NoActiveGoals_ReturnsFailure() [Fact] public async Task Handle_ReviewServiceFails_ReturnsFailure() { - _payGate.CanAccessGoals(UserId, Arg.Any()).Returns(Result.Success()); + _payGate.CanUseGoalReview(UserId, Arg.Any()).Returns(Result.Success()); var goal = CreateTestGoal(); _goalRepo.FindAsync( diff --git a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalsQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalsQueryHandlerTests.cs index 9df9798e..807fec88 100644 --- a/tests/Orbit.Application.Tests/Queries/Goals/GetGoalsQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Goals/GetGoalsQueryHandlerTests.cs @@ -12,7 +12,6 @@ namespace Orbit.Application.Tests.Queries.Goals; public class GetGoalsQueryHandlerTests { private readonly IGenericRepository _goalRepo = Substitute.For>(); - private readonly IPayGateService _payGate = Substitute.For(); private readonly IUserDateService _userDateService = Substitute.For(); private readonly IGoalProgressReadSyncer _goalProgressReadSyncer = Substitute.For(); private readonly GetGoalsQueryHandler _handler; @@ -22,9 +21,7 @@ public class GetGoalsQueryHandlerTests public GetGoalsQueryHandlerTests() { - _handler = new GetGoalsQueryHandler(_goalRepo, _payGate, _userDateService, _goalProgressReadSyncer); - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.Success()); + _handler = new GetGoalsQueryHandler(_goalRepo, _userDateService, _goalProgressReadSyncer); _userDateService.GetUserTodayAsync(UserId, Arg.Any()).Returns(Today); _goalProgressReadSyncer.ComputeFreshValuesAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new Dictionary()); @@ -148,25 +145,6 @@ public async Task Handle_TrackingStatus_OnTrackWhenDeadlineFar() result.Value.Items[0].TrackingStatus.Should().Be("on_track"); } - [Fact] - public async Task Handle_PaywalledUser_ReturnsPayGateFailure() - { - _payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Orbit.Domain.Common.Result.PayGateFailure("Goals are a Pro feature")); - - var result = await _handler.Handle(new GetGoalsQuery(UserId), CancellationToken.None); - - result.IsFailure.Should().BeTrue(); - result.ErrorCode.Should().Be(Orbit.Domain.Common.Result.PayGateErrorCode); - await _goalRepo.DidNotReceive().FindPagedAsync( - Arg.Any>>(), - Arg.Any, IOrderedQueryable>>(), - Arg.Any(), - Arg.Any(), - Arg.Any, IQueryable>?>(), - Arg.Any()); - } - [Fact] public async Task Handle_ComputesFreshStreakValuesBeforeReadingThem() { diff --git a/tests/Orbit.Infrastructure.Tests/Persistence/ApplyOnboardingConcurrencyTests.cs b/tests/Orbit.Infrastructure.Tests/Persistence/ApplyOnboardingConcurrencyTests.cs index bff0654d..f9fb8945 100644 --- a/tests/Orbit.Infrastructure.Tests/Persistence/ApplyOnboardingConcurrencyTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Persistence/ApplyOnboardingConcurrencyTests.cs @@ -45,7 +45,6 @@ public async Task Apply_ConflictOnFirstSave_RetriesAndAppliesExactlyOnce() new GenericRepository(context), new GenericRepository(context), new GenericRepository(context)), - Substitute.For(), StubToday(new DateOnly(2026, 7, 5)), Substitute.For(), unitOfWork, diff --git a/tests/Orbit.Infrastructure.Tests/Persistence/ConcurrencyRetryTests.cs b/tests/Orbit.Infrastructure.Tests/Persistence/ConcurrencyRetryTests.cs index a4ade18c..86fd99cd 100644 --- a/tests/Orbit.Infrastructure.Tests/Persistence/ConcurrencyRetryTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Persistence/ConcurrencyRetryTests.cs @@ -357,7 +357,6 @@ private static UpdateGoalProgressCommandHandler CreateGoalProgressHandler(OrbitD new GoalRepositories( goalRepository, new GenericRepository(context)), - PassingGoalGate(), completionService, unitOfWork, StubToday(new DateOnly(2026, 3, 20)), @@ -386,13 +385,6 @@ private static IPayGateService StubLimit() return payGate; } - private static IPayGateService PassingGoalGate() - { - var payGate = Substitute.For(); - payGate.CanAccessGoals(Arg.Any(), Arg.Any()).Returns(Result.Success()); - return payGate; - } - private sealed class ConflictOnceInterceptor(Action? onFirstSave = null) : SaveChangesInterceptor { public int SaveAttempts { get; private set; } diff --git a/tests/Orbit.Infrastructure.Tests/Services/AgentPolicyEvaluatorTests.cs b/tests/Orbit.Infrastructure.Tests/Services/AgentPolicyEvaluatorTests.cs index a6902edd..12f391a8 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/AgentPolicyEvaluatorTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/AgentPolicyEvaluatorTests.cs @@ -236,13 +236,8 @@ public void Evaluate_DestructiveChatCapability_OnChatSurface_RequiresConfirmatio } [Fact] - public void Evaluate_GoalsDelete_OnChatSurface_RequiresConfirmation() + public void Evaluate_GoalsDelete_FreeUserRequiresConfirmation() { - var proUser = _dbContext.Users.First(item => item.Id == _userId); - proUser.StartTrial(DateTime.UtcNow.AddDays(7)); - _dbContext.AppFeatureFlags.Add(AppFeatureFlag.Create("goal_tracking", true, "Pro", "Goal tracking")); - _dbContext.SaveChanges(); - var decision = _policyEvaluator.Evaluate(new AgentPolicyEvaluationContext( AgentCapabilityIds.GoalsDelete, _userId, @@ -254,6 +249,7 @@ public void Evaluate_GoalsDelete_OnChatSurface_RequiresConfirmation() OperationFingerprint: "delete_goal:{\"goalId\":\"123\"}")); decision.Status.Should().Be(AgentPolicyDecisionStatus.ConfirmationRequired); + decision.Reason.Should().Be("confirmation_required"); decision.PendingOperation.Should().NotBeNull(); } diff --git a/tests/Orbit.Infrastructure.Tests/Services/GoalCompletionServiceTests.cs b/tests/Orbit.Infrastructure.Tests/Services/GoalCompletionServiceTests.cs index 5746de72..ba84e3bd 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/GoalCompletionServiceTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/GoalCompletionServiceTests.cs @@ -91,7 +91,6 @@ public async Task UpdateGoalProgress_ManualCompletion_AwardsOnceAfterProgressFlu new GoalRepositories( new GenericRepository(dbContext), new GenericRepository(dbContext)), - SuccessfulPayGate(), CreateCompletionService(dbContext, gamification, unitOfWork), unitOfWork, StubToday(user.Id), @@ -170,7 +169,6 @@ public async Task LinkGoalsToHabit_TwoGoalsComplete_PersistsEachBeforeItsAward() var handler = new LinkGoalsToHabitCommandHandler( new GenericRepository(dbContext), new GenericRepository(dbContext), - SuccessfulPayGate(), CreateCompletionService(dbContext, gamification, unitOfWork), StubToday(user.Id)); @@ -208,7 +206,6 @@ public async Task LinkHabitsToGoal_AwardFailure_RollsBackAndRetryCompletes() var handler = new LinkHabitsToGoalCommandHandler( new GenericRepository(dbContext), new GenericRepository(dbContext), - SuccessfulPayGate(), CreateCompletionService(dbContext, gamification, unitOfWork), StubToday(user.Id), cache); @@ -332,16 +329,6 @@ private static IGamificationService CreateGamificationService( NullLogger.Instance); } - private static IPayGateService SuccessfulPayGate() - { - var payGate = Substitute.For(); - payGate.CanLinkGoalsToHabits(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); - payGate.CanAccessGoals(Arg.Any(), Arg.Any()) - .Returns(Result.Success()); - return payGate; - } - private static IUserDateService StubToday(Guid userId) { var userDateService = Substitute.For(); From 53b24a56b19c5c134a8c7987192d0bd3c1c29344 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Sun, 23 Aug 2026 17:42:14 -0300 Subject: [PATCH 3/4] Regenerate architecture map --- architecture.html | 2 +- architecture.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/architecture.html b/architecture.html index df661106..e9f34525 100644 --- a/architecture.html +++ b/architecture.html @@ -47,7 +47,7 @@

Handlers with no endpoint

RequestHandler file

Entities

EntityDomain file
- +