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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -4848,6 +4848,7 @@
"testClass": "PayGateServiceTests",
"file": "tests/Orbit.Application.Tests/Common/PayGateServiceTests.cs",
"references": [
"Goal",
"Habit",
"User"
]
Expand Down Expand Up @@ -7499,7 +7500,6 @@
"references": [
"ApiKey",
"AppFeatureFlag",
"Goal",
"User"
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

public class GoalReviewTool(
IGenericRepository<Goal> goalRepository,
IPayGateService payGate,
IUserDateService userDateService,
IGoalProgressReadSyncer goalProgressReadSyncer) : IAiTool
{
Expand All @@ -22,13 +23,17 @@

public async Task<ToolResult> ExecuteAsync(JsonElement args, Guid userId, CancellationToken ct)
{
var gateCheck = await payGate.CanUseGoalReview(userId, ct);
if (gateCheck.IsFailure)
return ToolResult.FromFailure(gateCheck);

var userToday = await userDateService.GetUserTodayAsync(userId, ct);
var freshValues = await goalProgressReadSyncer.ComputeFreshValuesAsync(userId, userToday, ct);
var streakWindowStart = userToday.AddDays(-AppConstants.MaxStreakLookbackDays);

var goals = await goalRepository.FindAsync(
g => g.UserId == userId && g.Status == GoalStatus.Active,
q => q.Include(g => g.ProgressLogs).Include(g => g.Habits).ThenInclude(h => h.Logs.Where(l => l.Date >= streakWindowStart)),

Check warning on line 36 in src/Orbit.Application/Chat/Tools/Implementations/GoalReviewTool.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).
ct);

if (goals.Count == 0)
Expand Down
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 @@ -142,21 +142,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 @@ -191,9 +178,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 @@ -149,13 +149,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 @@ -37,13 +37,6 @@
{
public async Task<Result> 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);
Expand Down
Loading
Loading