Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Orbit.Application/Common/AppConfigKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
20 changes: 2 additions & 18 deletions src/Orbit.Application/Common/PayGateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,8 @@ public async Task<Result> CanUseRetrospective(Guid userId, CancellationToken ct
return Result.Success();
}

public async Task<Result> 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<Result> CanCreateGoals(Guid userId, CancellationToken ct = default) =>
CanAccessGoals(userId, ct);
public Task<Result> CanUseGoalReview(Guid userId, CancellationToken ct = default) =>
RequireProAccess(userId, "Goal reviews are a Pro feature. Upgrade to unlock!", ct);

public Task<Result> CanAccessCalendar(Guid userId, CancellationToken ct = default) =>
RequireProAccess(userId, "Calendar integration is a Pro feature. Upgrade to unlock!", ct);
Expand Down Expand Up @@ -194,9 +181,6 @@ public Task<Result> CanManageUserFacts(Guid userId, CancellationToken ct = defau
public Task<Result> CanUseSlipAlerts(Guid userId, CancellationToken ct = default) =>
RequireProAccess(userId, "Slip alerts are a Pro feature. Upgrade to unlock!", ct);

public Task<Result> CanLinkGoalsToHabits(Guid userId, CancellationToken ct = default) =>
CanAccessGoals(userId, ct);

public async Task<Result> CanCreateApiKeys(Guid userId, CancellationToken ct = default)
{
return await CanManageApiKeys(userId, ct);
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
GoalType Type = GoalType.Standard,
IReadOnlyList<Guid>? HabitIds = null) : IRequest<Result<Guid>>, IIdempotentCommand;

public partial class CreateGoalCommandHandler(

Check warning on line 25 in src/Orbit.Application/Goals/Commands/CreateGoalCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Constructor has 8 parameters, which is greater than the 7 authorized.
IGenericRepository<Goal> goalRepository,
IGenericRepository<Habit> habitRepository,
IPayGateService payGate,
IUserDateService userDateService,
IGamificationService gamificationService,
IGoalCompletionService goalCompletionService,
Expand All @@ -35,10 +34,6 @@
{
public async Task<Result<Guid>> Handle(CreateGoalCommand request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<Guid>();

var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken);
if (request.Deadline is { } deadline && deadline < today)
return Result.Failure<Guid>(ErrorMessages.DeadlineInPast);
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Commands/DeleteGoalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@ public record DeleteGoalCommand(

public class DeleteGoalCommandHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUnitOfWork unitOfWork,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<DeleteGoalCommand, Result>
{
public async Task<Result> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@ public record LinkHabitsToGoalCommand(
public class LinkHabitsToGoalCommandHandler(
IGenericRepository<Goal> goalRepository,
IGenericRepository<Habit> habitRepository,
IPayGateService payGate,
IGoalCompletionService goalCompletionService,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<LinkHabitsToGoalCommand, Result>
{
public async Task<Result> 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));

Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Commands/ReorderGoalsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,12 @@ public record ReorderGoalsCommand(

public class ReorderGoalsCommandHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUnitOfWork unitOfWork,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<ReorderGoalsCommand, Result>
{
public async Task<Result> 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(
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Commands/RestoreGoalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@ public record RestoreGoalCommand(

public class RestoreGoalCommandHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUnitOfWork unitOfWork,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<RestoreGoalCommand, Result>
{
public async Task<Result> 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);
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Commands/UpdateGoalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,13 @@ public record UpdateGoalCommand(

public class UpdateGoalCommandHandler(
GoalRepositories repos,
IPayGateService payGate,
IUserDateService userDateService,
IGoalCompletionService goalCompletionService,
IUnitOfWork unitOfWork,
IMemoryCache cache) : IRequestHandler<UpdateGoalCommand, Result>
{
public async Task<Result> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@ public record UpdateGoalProgressCommand(

public class UpdateGoalProgressCommandHandler(
GoalRepositories repos,
IPayGateService payGate,
IGoalCompletionService goalCompletionService,
IUnitOfWork unitOfWork,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<UpdateGoalProgressCommand, Result>
{
public async Task<Result> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@ public record UpdateGoalStatusCommand(

public class UpdateGoalStatusCommandHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IGoalCompletionService goalCompletionService,
IUnitOfWork unitOfWork,
IUserDateService userDateService,
IMemoryCache cache) : IRequestHandler<UpdateGoalStatusCommand, Result>
{
public async Task<Result> 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);
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Queries/GetGoalByIdQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,10 @@ public record GetGoalByIdQuery(

public class GetGoalByIdQueryHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUserDateService userDateService) : IRequestHandler<GetGoalByIdQuery, Result<GoalDetailDto>>
{
public async Task<Result<GoalDetailDto>> Handle(GetGoalByIdQuery request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<GoalDetailDto>();

var loaded = await GoalDetailLoader.BuildGoalDetailAsync(
goalRepository, userDateService, request.GoalId, request.UserId, cancellationToken);
if (loaded is null)
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Queries/GetGoalDetailQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,10 @@ public record GetGoalDetailQuery(

public class GetGoalDetailQueryHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUserDateService userDateService) : IRequestHandler<GetGoalDetailQuery, Result<GoalDetailWithMetricsResponse>>
{
public async Task<Result<GoalDetailWithMetricsResponse>> Handle(GetGoalDetailQuery request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<GoalDetailWithMetricsResponse>();

var loaded = await GoalDetailLoader.BuildGoalDetailAsync(
goalRepository, userDateService, request.GoalId, request.UserId, cancellationToken);
if (loaded is null)
Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,11 @@

public class GetGoalMetricsQueryHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUserDateService userDateService,
IGoalProgressReadSyncer goalProgressReadSyncer) : IRequestHandler<GetGoalMetricsQuery, Result<GoalMetrics>>
{
public async Task<Result<GoalMetrics>> Handle(GetGoalMetricsQuery request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<GoalMetrics>();

var userToday = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken);
var freshValues = await goalProgressReadSyncer.ComputeFreshValuesAsync(
request.UserId,
Expand All @@ -32,7 +27,7 @@

var goals = await goalRepository.FindAsync(
g => g.Id == request.GoalId && g.UserId == request.UserId,
q => q.Include(g => g.ProgressLogs)

Check warning on line 30 in src/Orbit.Application/Goals/Queries/GetGoalMetricsQuery.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Call 'AsSplitQuery' to avoid multiplying rows by including 'ProgressLogs' and 'Habits' in the same query (a Cartesian explosion).
.Include(g => g.Habits).ThenInclude(h => h.Logs.Where(l => l.Date >= streakWindowStart)),
cancellationToken);
var goal = goals.Count > 0 ? goals[0] : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,14 @@ public record GetGoalProgressHistoryQuery(

/// <summary>
/// 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.
/// </summary>
public class GetGoalProgressHistoryQueryHandler(
IGenericRepository<Goal> goalRepository,
IGenericRepository<GoalProgressLog> progressLogRepository,
IPayGateService payGate) : IRequestHandler<GetGoalProgressHistoryQuery, Result<GoalProgressHistoryResponse>>
IGenericRepository<GoalProgressLog> progressLogRepository) : IRequestHandler<GetGoalProgressHistoryQuery, Result<GoalProgressHistoryResponse>>
{
public async Task<Result<GoalProgressHistoryResponse>> Handle(GetGoalProgressHistoryQuery request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<GoalProgressHistoryResponse>();

var goalExists = await goalRepository.AnyAsync(
g => g.Id == request.GoalId && g.UserId == request.UserId,
cancellationToken);
Expand Down
2 changes: 1 addition & 1 deletion src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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<GoalReviewResponse>();

Expand All @@ -44,7 +44,7 @@

var goals = await goalRepository.FindAsync(
g => g.UserId == request.UserId && g.Status == GoalStatus.Active,
q => q.Include(g => g.ProgressLogs)

Check warning on line 47 in src/Orbit.Application/Goals/Queries/GetGoalReviewQuery.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Call 'AsSplitQuery' to avoid multiplying rows by including 'ProgressLogs' and 'Habits' in the same query (a Cartesian explosion).
.Include(g => g.Habits).ThenInclude(h => h.Logs.Where(l => l.Date >= streakWindowStart)),
cancellationToken);

Expand Down
5 changes: 0 additions & 5 deletions src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,11 @@ public record GetGoalsQuery(

public class GetGoalsQueryHandler(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUserDateService userDateService,
IGoalProgressReadSyncer goalProgressReadSyncer) : IRequestHandler<GetGoalsQuery, Result<PaginatedResponse<GoalDto>>>
{
public async Task<Result<PaginatedResponse<GoalDto>>> Handle(GetGoalsQuery request, CancellationToken cancellationToken)
{
var gateCheck = await payGate.CanAccessGoals(request.UserId, cancellationToken);
if (gateCheck.IsFailure)
return gateCheck.PropagateError<PaginatedResponse<GoalDto>>();

var userToday = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken);
var freshProgressValues = await goalProgressReadSyncer.ComputeFreshValuesAsync(request.UserId, userToday, cancellationToken);

Expand Down
7 changes: 0 additions & 7 deletions src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
IGenericRepository<Tag> TagRepository,
IGenericRepository<Goal> GoalRepository);

public partial class CreateHabitCommandHandler(

Check warning on line 37 in src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Constructor has 8 parameters, which is greater than the 7 authorized.
CreateHabitRepositories repos,
IUserDateService userDateService,
IPayGateService payGate,
Expand Down Expand Up @@ -140,13 +140,6 @@
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@ public record LinkGoalsToHabitCommand(
public class LinkGoalsToHabitCommandHandler(
IGenericRepository<Habit> habitRepository,
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IGoalCompletionService goalCompletionService,
IUserDateService userDateService) : IRequestHandler<LinkGoalsToHabitCommand, Result>
{
public async Task<Result> 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),
Expand Down
7 changes: 0 additions & 7 deletions src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
IReadOnlyList<Guid>? GoalIds = null,
string? Emoji = null) : IRequest<Result>;

public class UpdateHabitCommandHandler(

Check warning on line 28 in src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Constructor has 8 parameters, which is greater than the 7 authorized.
IGenericRepository<Habit> habitRepository,
IGenericRepository<SentReminder> sentReminderRepository,
IGenericRepository<Goal> goalRepository,
Expand All @@ -35,15 +35,8 @@
IUnitOfWork unitOfWork,
IMemoryCache cache) : IRequestHandler<UpdateHabitCommand, Result>
{
public async Task<Result> Handle(UpdateHabitCommand request, CancellationToken cancellationToken)

Check warning on line 38 in src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
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);
Expand Down
11 changes: 3 additions & 8 deletions src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public record ApplyOnboardingCommand(
/// <summary>
/// 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 <c>HasCompletedOnboarding</c>. Idempotent
/// goal, week-start/color preferences, and flips <c>HasCompletedOnboarding</c>. Idempotent
/// by construction — an already-onboarded user is a no-op (<c>Applied:false</c>) — so the client can
/// flush unconditionally after any successful auth and retry safely under the concurrency pipeline.
/// </summary>
Expand All @@ -65,7 +65,6 @@ public record ApplyOnboardingRepositories(

public class ApplyOnboardingCommandHandler(
ApplyOnboardingRepositories repos,
IPayGateService payGate,
IUserDateService userDateService,
IAppConfigService appConfig,
IUnitOfWork unitOfWork,
Expand Down Expand Up @@ -121,7 +120,7 @@ private async Task<Result<ApplyOnboardingResponse>> 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<ApplyOnboardingResponse>();
var createdGoal = goalResult.Value;
Expand Down Expand Up @@ -189,16 +188,12 @@ private async Task<Result<List<Habit>>> CreateHabitsAsync(
return Result.Success(createdHabits);
}

private async Task<Result<bool>> CreateGoalIfAllowedAsync(
private async Task<Result<bool>> 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<bool>(ErrorMessages.DeadlineInPast);

Expand Down
Loading
Loading