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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Orbit.Application.Chat.Tools.Implementations;

public class UpdateHabitTool(
IGenericRepository<Habit> habitRepository,
IUserDateService userDateService,
IPayGateService? payGate = null) : IAiTool
{
public string Name => "update_habit";
Expand Down Expand Up @@ -96,7 +97,8 @@ public async Task<ToolResult> ExecuteAsync(JsonElement args, Guid userId, Cancel
if (habit is null)
return HabitToolHelpers.HabitNotFoundResult(habitId);

var updateParams = ResolveUpdateParams(args, habit);
var today = await userDateService.GetUserTodayAsync(userId, ct);
var updateParams = ResolveUpdateParams(args, habit, today);

var result = await HabitReactivationAllowance.ExecuteAsync(
userId,
Expand All @@ -119,7 +121,7 @@ public async Task<ToolResult> ExecuteAsync(JsonElement args, Guid userId, Cancel
/// Resolve each field: absent = keep existing, null = clear, value = update.
/// Extracted to reduce ExecuteAsync cognitive complexity.
/// </summary>
private static HabitUpdateParams ResolveUpdateParams(JsonElement args, Habit habit)
private static HabitUpdateParams ResolveUpdateParams(JsonElement args, Habit habit, DateOnly today)
{
var title = ResolveTitle(args, habit);
var description = ResolveDescription(args, habit);
Expand Down Expand Up @@ -148,7 +150,8 @@ private static HabitUpdateParams ResolveUpdateParams(JsonElement args, Habit hab
EndDate: endDate,
ClearEndDate: clearEndDate,
ScheduledReminders: scheduledReminders,
Emoji: ResolveEmoji(args, habit));
Emoji: ResolveEmoji(args, habit),
UserToday: today);
}

private static string ResolveTitle(JsonElement args, Habit habit) =>
Expand Down
5 changes: 3 additions & 2 deletions src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
IUnitOfWork unitOfWork,
IMemoryCache cache) : IRequestHandler<UpdateHabitCommand, Result>
{
public async Task<Result> Handle(UpdateHabitCommand request, CancellationToken cancellationToken)

Check warning on line 36 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 18 to the 15 allowed.
{
if (request.GoalIds is not null)
{
Expand Down Expand Up @@ -70,6 +70,7 @@
}

var opts = request.Options ?? new UpdateHabitCommandOptions();
var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken);

var result = await HabitReactivationAllowance.ExecuteAsync(
request.UserId,
Expand Down Expand Up @@ -99,7 +100,8 @@
EndDate: opts.EndDate,
ClearEndDate: request.ClearEndDate,
ScheduledReminders: opts.ScheduledReminders,
Emoji: request.Emoji)),
Emoji: request.Emoji,
UserToday: today)),
cancellationToken);

if (result.IsFailure)
Expand All @@ -117,7 +119,6 @@

await unitOfWork.SaveChangesAsync(cancellationToken);

var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken);
CacheInvalidationHelper.InvalidateUserAiCaches(cache, request.UserId, today);

return Result.Success();
Expand Down
51 changes: 48 additions & 3 deletions src/Orbit.Application/Habits/Services/HabitMetricsCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static HabitMetrics Calculate(
TimeZoneInfo? userTimeZone = null)
{
var logDates = logs.Where(l => l.Value > 0).Select(l => l.Date).Distinct().ToHashSet();
var expectedDates = GenerateExpectedDates(habit, today, userTimeZone).ToList();
var expectedDates = GenerateExpectedDates(habit, logs, today, userTimeZone).ToList();

var currentStreak = CalculateCurrentStreak(habit, expectedDates, logDates, today);
var longestStreak = CalculateLongestStreak(habit, expectedDates, logDates);
Expand Down Expand Up @@ -49,10 +49,16 @@ public static DateOnly GetUserToday(User user)
return DateOnly.FromDateTime(userNow);
}

private static List<DateOnly> GenerateExpectedDates(Habit habit, DateOnly today, TimeZoneInfo? userTimeZone = null)
private static List<DateOnly> GenerateExpectedDates(
Habit habit,
IReadOnlyCollection<HabitLog> logs,
DateOnly today,
TimeZoneInfo? userTimeZone = null)
{
var tz = userTimeZone ?? TimeZoneInfo.Utc;
var habitStartDate = DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(habit.CreatedAtUtc, tz));
var createdDate = DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(habit.CreatedAtUtc, tz));
var habitStartDate = habit.ScheduledStartDate
?? ResolveLegacyStartDate(habit, logs, createdDate);

if (habit.FrequencyUnit is null || habit.FrequencyQuantity is null)
return [habitStartDate];
Expand All @@ -63,6 +69,45 @@ private static List<DateOnly> GenerateExpectedDates(Habit habit, DateOnly today,
return GenerateFrequencyBasedDates(habit, today, habitStartDate);
}

private static DateOnly ResolveLegacyStartDate(
Habit habit,
IReadOnlyCollection<HabitLog> logs,
DateOnly createdDate)
{
var hasProgressingHistory = HasProgressingLegacyHistory(
habit,
logs,
createdDate);
return hasProgressingHistory ? createdDate : habit.DueDate;
}

private static bool HasProgressingLegacyHistory(
Habit habit,
IReadOnlyCollection<HabitLog> logs,
DateOnly createdDate)
{
if (habit.FrequencyUnit is null || habit.IsBadHabit)
return false;
Comment thread
thomasluizon marked this conversation as resolved.

var resolvedDates = logs
.Where(log => !log.IsDeleted)
.Select(log => log.Date)
.ToHashSet();
var firstCandidate = createdDate < habit.DueDate.AddDays(-MaxStreakHorizonDays)
? habit.DueDate.AddDays(-MaxStreakHorizonDays)
: createdDate;
var candidateCount = habit.DueDate.DayNumber - firstCandidate.DayNumber;
if (candidateCount <= 0)
return false;

var expectedBeforeDue = Enumerable.Range(0, candidateCount)
.Select(firstCandidate.AddDays)
.Where(date => HabitScheduleService.IsHabitHistoricallyDueOnDate(habit, date, createdDate))
.ToList();

return expectedBeforeDue.Count > 0 && expectedBeforeDue.All(resolvedDates.Contains);
Comment thread
thomasluizon marked this conversation as resolved.
}

private static List<DateOnly> GenerateDayFilteredDates(Habit habit, DateOnly today, DateOnly startDate)
{
var expectedDates = new List<DateOnly>();
Expand Down
26 changes: 25 additions & 1 deletion src/Orbit.Domain/Entities/Habit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public record HabitUpdateParams(
DateOnly? EndDate = null,
bool? ClearEndDate = null,
IReadOnlyList<ScheduledReminderTime>? ScheduledReminders = null,
string? Emoji = null);
string? Emoji = null,
DateOnly? UserToday = null);

public class Habit : Entity, ITimestamped, ISoftDeletable
{
Expand All @@ -62,6 +63,7 @@ public class Habit : Entity, ITimestamped, ISoftDeletable
public bool IsBadHabit { get; private set; }
public bool IsCompleted { get; private set; }
public DateOnly DueDate { get; private set; }
public DateOnly? ScheduledStartDate { get; private set; }
/// <summary>
/// For monthly/yearly habits, the original day-of-month from the first DueDate (1-31).
/// Preserves the anchor across month-end clamping so a Jan 31 habit re-anchors to Mar 31
Expand Down Expand Up @@ -146,6 +148,7 @@ public static Result<Habit> Create(HabitCreateParams p)
IsGeneral = p.IsGeneral,
IsFlexible = p.IsFlexible,
DueDate = p.DueDate,
ScheduledStartDate = p.DueDate,
Comment thread
thomasluizon marked this conversation as resolved.
OriginalDayOfMonth = p.FrequencyUnit is Enums.FrequencyUnit.Month or Enums.FrequencyUnit.Year
? p.DueDate.Day
: null,
Expand Down Expand Up @@ -193,6 +196,8 @@ public Result<HabitLog> Log(DateOnly date, string? note = null, bool advanceDueD

public void AdvanceDueDate(DateOnly today)
{
CaptureLegacyScheduledStart();

do
{
var prev = DueDate;
Expand All @@ -214,6 +219,9 @@ public void AdvanceDueDate(DateOnly today)
/// </summary>
public void CatchUpDueDate(DateOnly today)
{
if (DueDate < today && !IsCompleted)
CaptureLegacyScheduledStart();

while (DueDate < today && !IsCompleted)
{
var prev = DueDate;
Expand All @@ -230,6 +238,12 @@ public void CatchUpDueDate(DateOnly today)
UpdatedAtUtc = DateTime.UtcNow;
}

private void CaptureLegacyScheduledStart()
{
if (ScheduledStartDate is null)
ScheduledStartDate = DueDate;
}

/// <summary>
/// Advances DueDate by one frequency step, re-anchoring for monthly/yearly drift
/// and snapping to the next matching day-of-week if Days are set.
Expand Down Expand Up @@ -368,7 +382,17 @@ private void ApplyRequiredUpdates(HabitUpdateParams p)
DueEndTime = p.DueEndTime;

if (p.DueDate is not null)
{
var reschedulesUnstartedHabit = ScheduledStartDate.HasValue
&& p.UserToday.HasValue
&& ScheduledStartDate.Value >= p.UserToday.Value
&& DueDate == ScheduledStartDate.Value
&& p.DueDate.Value != DueDate;
Comment thread
thomasluizon marked this conversation as resolved.
Comment thread
thomasluizon marked this conversation as resolved.

DueDate = p.DueDate.Value;
if (reschedulesUnstartedHabit)
ScheduledStartDate = DueDate;
}

if (FrequencyUnit is Enums.FrequencyUnit.Month or Enums.FrequencyUnit.Year)
OriginalDayOfMonth = DueDate.Day;
Expand Down
Loading
Loading