diff --git a/src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs b/src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs index 759160a7..9c63e364 100644 --- a/src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs +++ b/src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs @@ -188,10 +188,10 @@ private static Domain.Common.Result BuildParentHabit( return Habit.Create(new HabitCreateParams( userId, title, frequencyUnit, frequencyQuantity, + JsonArgumentParser.ParseDateOnly(args, "due_date") ?? today, JsonArgumentParser.GetOptionalString(args, "description"), Days: JsonArgumentParser.ParseDays(args), IsBadHabit: isBadHabit, - DueDate: JsonArgumentParser.ParseDateOnly(args, "due_date") ?? today, DueTime: JsonArgumentParser.ParseTimeOnly(args, "due_time"), ReminderEnabled: JsonArgumentParser.GetOptionalBool(args, "reminder_enabled") ?? false, ReminderTimes: JsonArgumentParser.ParseIntArray(args, "reminder_times"), @@ -244,10 +244,10 @@ private static Domain.Common.Result BuildChildHabit( JsonArgumentParser.GetOptionalString(sub, TitleProperty) ?? "Untitled", JsonArgumentParser.ParseFrequencyUnit(sub) ?? parentFreqUnit, JsonArgumentParser.GetOptionalInt(sub, "frequency_quantity") ?? parentFreqQty, + JsonArgumentParser.ParseDateOnly(sub, "due_date") ?? parentDueDate, JsonArgumentParser.GetOptionalString(sub, "description"), Days: subDays, IsBadHabit: JsonArgumentParser.GetOptionalBool(sub, "is_bad_habit") ?? false, - DueDate: JsonArgumentParser.ParseDateOnly(sub, "due_date") ?? parentDueDate, ParentHabitId: parentId, Emoji: JsonArgumentParser.GetOptionalString(sub, "emoji"), ChecklistItems: JsonArgumentParser.ParseChecklistItems(sub))); diff --git a/src/Orbit.Application/Habits/Commands/BulkCreateHabitsCommand.cs b/src/Orbit.Application/Habits/Commands/BulkCreateHabitsCommand.cs index bf85028f..3ad4d205 100644 --- a/src/Orbit.Application/Habits/Commands/BulkCreateHabitsCommand.cs +++ b/src/Orbit.Application/Habits/Commands/BulkCreateHabitsCommand.cs @@ -122,11 +122,11 @@ private async Task CreateSingleHabit( item.Title, item.FrequencyUnit, item.FrequencyQuantity, + item.DueDate ?? userToday, item.Description, Emoji: item.Emoji, Days: item.Days, IsBadHabit: item.IsBadHabit, - DueDate: item.DueDate ?? userToday, DueTime: item.DueTime, DueEndTime: item.DueEndTime, ReminderEnabled: item.ReminderEnabled, @@ -163,11 +163,11 @@ private async Task CreateSingleHabit( subItem.Title, subItem.FrequencyUnit ?? item.FrequencyUnit, subItem.FrequencyQuantity ?? item.FrequencyQuantity, + subItem.DueDate ?? item.DueDate ?? userToday, subItem.Description, Emoji: subItem.Emoji, Days: subItem.Days ?? item.Days, IsBadHabit: subItem.IsBadHabit, - DueDate: subItem.DueDate ?? item.DueDate ?? userToday, ParentHabitId: parentHabit.Id, IsGeneral: item.IsGeneral, IsFlexible: subItem.IsFlexible, diff --git a/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs b/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs index 44bf8aab..635dfb61 100644 --- a/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs @@ -90,11 +90,11 @@ public async Task> Handle(CreateHabitCommand request, CancellationT request.Title, request.FrequencyUnit, request.FrequencyQuantity, + dueDate, request.Description, Emoji: request.Emoji, Days: opts.Days, IsBadHabit: request.IsBadHabit, - DueDate: dueDate, DueTime: opts.DueTime, DueEndTime: opts.DueEndTime, ReminderEnabled: opts.ReminderEnabled, @@ -145,7 +145,7 @@ private async Task CreateSubHabitsAsync( subTitle, request.FrequencyUnit, request.FrequencyQuantity, - DueDate: request.DueDate ?? dueDate, + request.DueDate ?? dueDate, ParentHabitId: parentId, IsGeneral: request.IsGeneral, EndDate: opts.EndDate, diff --git a/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs b/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs index 28337159..d06bf940 100644 --- a/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs @@ -66,11 +66,11 @@ public async Task> Handle(CreateSubHabitCommand request, Cancellati request.Title, request.FrequencyUnit ?? parent.FrequencyUnit, request.FrequencyQuantity ?? parent.FrequencyQuantity, + childDueDate, request.Description, Emoji: request.Emoji, Days: opts.Days, IsBadHabit: request.IsBadHabit, - DueDate: childDueDate, DueTime: opts.DueTime, DueEndTime: opts.DueEndTime, ParentHabitId: parent.Id, diff --git a/src/Orbit.Application/Habits/Commands/DuplicateHabitCommand.cs b/src/Orbit.Application/Habits/Commands/DuplicateHabitCommand.cs index df4d7b75..a62f4b86 100644 --- a/src/Orbit.Application/Habits/Commands/DuplicateHabitCommand.cs +++ b/src/Orbit.Application/Habits/Commands/DuplicateHabitCommand.cs @@ -130,11 +130,11 @@ private static Result CloneHabit( source.Title, source.FrequencyUnit, source.FrequencyQuantity, + source.DueDate, source.Description, Emoji: source.Emoji, Days: source.Days.ToList(), IsBadHabit: source.IsBadHabit, - DueDate: source.DueDate, DueTime: source.DueTime, DueEndTime: source.DueEndTime, ParentHabitId: parentHabitId, diff --git a/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs b/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs index dc5f1527..db4ae6ae 100644 --- a/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs +++ b/src/Orbit.Application/Profile/Commands/ApplyOnboardingCommand.cs @@ -155,11 +155,11 @@ private async Task>> CreateHabitsAsync( item.Title, item.FrequencyUnit, item.FrequencyQuantity, + item.DueDate ?? today, item.Description, Emoji: item.Emoji, Days: item.Days, IsBadHabit: item.IsBadHabit, - DueDate: item.DueDate ?? today, DueTime: item.DueTime, ReminderEnabled: item.ReminderEnabled, ReminderTimes: item.ReminderTimes, diff --git a/src/Orbit.Domain/Entities/Habit.cs b/src/Orbit.Domain/Entities/Habit.cs index 48173b51..f6bce784 100644 --- a/src/Orbit.Domain/Entities/Habit.cs +++ b/src/Orbit.Domain/Entities/Habit.cs @@ -11,10 +11,10 @@ public record HabitCreateParams( string Title, FrequencyUnit? FrequencyUnit, int? FrequencyQuantity, + DateOnly DueDate, string? Description = null, IReadOnlyList? Days = null, bool IsBadHabit = false, - DateOnly? DueDate = null, TimeOnly? DueTime = null, TimeOnly? DueEndTime = null, Guid? ParentHabitId = null, @@ -129,8 +129,6 @@ public static Result Create(HabitCreateParams p) if (reminderValidation is not null) return Result.Failure(reminderValidation); - var effectiveDueDate = p.DueDate ?? DateOnly.FromDateTime(DateTime.UtcNow); - return Result.Success(new Habit { UserId = p.UserId, @@ -143,9 +141,9 @@ public static Result Create(HabitCreateParams p) IsBadHabit = p.IsBadHabit, IsGeneral = p.IsGeneral, IsFlexible = p.IsFlexible, - DueDate = effectiveDueDate, + DueDate = p.DueDate, OriginalDayOfMonth = p.FrequencyUnit is Enums.FrequencyUnit.Month or Enums.FrequencyUnit.Year - ? effectiveDueDate.Day + ? p.DueDate.Day : null, DueTime = p.DueTime, DueEndTime = p.DueEndTime, diff --git a/src/Orbit.Domain/Entities/HabitInvariants.cs b/src/Orbit.Domain/Entities/HabitInvariants.cs index b21f6801..102e4c1f 100644 --- a/src/Orbit.Domain/Entities/HabitInvariants.cs +++ b/src/Orbit.Domain/Entities/HabitInvariants.cs @@ -41,7 +41,7 @@ internal static class HabitInvariants public static AppError? ValidateDateOptions( TimeOnly? dueTime, TimeOnly? dueEndTime, DateOnly? endDate, FrequencyUnit? frequencyUnit, - bool isGeneral, DateOnly? dueDate) + bool isGeneral, DateOnly dueDate) { if (dueEndTime.HasValue && dueTime.HasValue && dueEndTime.Value <= dueTime.Value) return DomainErrors.EndTimeBeforeStartTime; @@ -49,8 +49,7 @@ internal static class HabitInvariants if (endDate.HasValue && frequencyUnit is null && !isGeneral) return DomainErrors.OneTimeTaskHasEndDate; - var effectiveDueDate = dueDate ?? DateOnly.FromDateTime(DateTime.UtcNow); - if (endDate.HasValue && endDate.Value < effectiveDueDate) + if (endDate.HasValue && endDate.Value < dueDate) return DomainErrors.EndDateBeforeStartDate; return null; diff --git a/tests/Orbit.Application.Tests/Chat/QueryHabitsToolTests.cs b/tests/Orbit.Application.Tests/Chat/QueryHabitsToolTests.cs index c33e1113..47be0115 100644 --- a/tests/Orbit.Application.Tests/Chat/QueryHabitsToolTests.cs +++ b/tests/Orbit.Application.Tests/Chat/QueryHabitsToolTests.cs @@ -454,7 +454,7 @@ private static Habit CreateHabit( string? emoji = null) { var habit = Habit.Create(new HabitCreateParams(UserId, title, freq, qty, - DueDate: dueDate, IsBadHabit: isBadHabit, IsGeneral: isGeneral, + DueDate: dueDate ?? DateOnly.FromDateTime(DateTime.UtcNow), IsBadHabit: isBadHabit, IsGeneral: isGeneral, ParentHabitId: parentId, Emoji: emoji)).Value; if (position.HasValue) habit.SetPosition(position.Value); return habit; diff --git a/tests/Orbit.Application.Tests/Commands/Calendar/RunCalendarAutoSyncCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Calendar/RunCalendarAutoSyncCommandHandlerTests.cs index d4bdd610..6d5d11a3 100644 --- a/tests/Orbit.Application.Tests/Commands/Calendar/RunCalendarAutoSyncCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Calendar/RunCalendarAutoSyncCommandHandlerTests.cs @@ -185,6 +185,7 @@ public async Task Handle_Success_DedupesAgainstExistingHabits() var existingHabit = Habit.Create(new HabitCreateParams( user.Id, "Daily standup", FrequencyUnit.Day, 1, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), GoogleEventId: "evt_a")).Value; _habitRepo.FindAsync(Arg.Any>>(), Arg.Any()) .Returns(new List { existingHabit }.AsReadOnly()); @@ -659,7 +660,7 @@ public async Task Handle_UniqueViolationButNoNewSuggestions_Propagates() .Returns(new GoogleTokenRefreshOutcome("new_access", GoogleTokenRefreshResult.Success, null)); var existingHabit = Habit.Create(new HabitCreateParams( - user.Id, "Standup", FrequencyUnit.Day, 1, GoogleEventId: "evt_a")).Value; + user.Id, "Standup", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), GoogleEventId: "evt_a")).Value; _habitRepo.FindAsync(Arg.Any>>(), Arg.Any()) .Returns(new List { existingHabit }.AsReadOnly()); _fetcher.FetchAsync(Arg.Any(), Arg.Any?>(), Arg.Any(), Arg.Any()) diff --git a/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs index 42666292..2e98a61a 100644 --- a/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Goals/LinkHabitsToGoalCommandHandlerTests.cs @@ -32,8 +32,8 @@ public LinkHabitsToGoalCommandHandlerTests() public async Task Handle_ValidLink_LinksHabitsToGoal() { var goal = Goal.Create(UserId, "Goal", 100, "km").Value; - var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _goalRepo.FindOneTrackedAsync( Arg.Any>>(), @@ -91,7 +91,7 @@ public async Task Handle_TooManyHabits_ReturnsFailure() public async Task Handle_EmptyHabitList_ClearsExistingLinks() { var goal = Goal.Create(UserId, "Goal", 100, "km").Value; - var existingHabit = Habit.Create(new HabitCreateParams(UserId, "Existing", FrequencyUnit.Day, 1)).Value; + var existingHabit = Habit.Create(new HabitCreateParams(UserId, "Existing", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(existingHabit); _goalRepo.FindOneTrackedAsync( @@ -117,8 +117,8 @@ public async Task Handle_EmptyHabitList_ClearsExistingLinks() public async Task Handle_ReplacesExistingLinks() { var goal = Goal.Create(UserId, "Goal", 100, "km").Value; - var oldHabit = Habit.Create(new HabitCreateParams(UserId, "Old", FrequencyUnit.Day, 1)).Value; - var newHabit = Habit.Create(new HabitCreateParams(UserId, "New", FrequencyUnit.Day, 1)).Value; + var oldHabit = Habit.Create(new HabitCreateParams(UserId, "Old", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var newHabit = Habit.Create(new HabitCreateParams(UserId, "New", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(oldHabit); _goalRepo.FindOneTrackedAsync( @@ -161,7 +161,7 @@ public async Task Handle_PaywalledUser_ReturnsPayGateFailure() public async Task Handle_ForeignOrMissingHabitId_ReturnsFailureWithoutClearing() { var goal = Goal.Create(UserId, "Goal", 100, "km").Value; - var existingHabit = Habit.Create(new HabitCreateParams(UserId, "Existing", FrequencyUnit.Day, 1)).Value; + var existingHabit = Habit.Create(new HabitCreateParams(UserId, "Existing", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(existingHabit); _goalRepo.FindOneTrackedAsync( diff --git a/tests/Orbit.Application.Tests/Commands/Habits/BulkDeleteHabitsCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/BulkDeleteHabitsCommandHandlerTests.cs index 85fbc2f9..6ef1c6f1 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/BulkDeleteHabitsCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/BulkDeleteHabitsCommandHandlerTests.cs @@ -40,8 +40,8 @@ public BulkDeleteHabitsCommandHandlerTests() [Fact] public async Task Handle_ValidIds_DeletesAllHabits() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _habitRepo.FindTrackedAsync( Arg.Any>>(), @@ -66,7 +66,7 @@ await _unitOfWork.Received(1).ExecuteInTransactionAsync( [Fact] public async Task Handle_SomeNotFound_ReportsPartialFailure() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var missingId = Guid.NewGuid(); _habitRepo.FindTrackedAsync( @@ -105,7 +105,7 @@ public async Task Handle_AllNotFound_ReportsAllFailed() [Fact] public async Task Handle_InvalidatesSummaryCache() { - var habit = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _habitRepo.FindTrackedAsync( Arg.Any>>(), Arg.Any()) diff --git a/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs index 36d909ea..c8c31e7d 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/CreateHabitCommandHandlerTests.cs @@ -324,6 +324,25 @@ await _habitRepo.Received(1).AddAsync( Arg.Any()); } + [Fact] + public async Task Handle_UserTimezoneAheadOfUtc_UsesUserTodayNotUtcToday() + { + var utcToday = DateOnly.FromDateTime(DateTime.UtcNow); + var userToday = utcToday.AddDays(1); + _userDateService.GetUserTodayAsync(Arg.Any(), Arg.Any()) + .Returns(userToday); + + var command = new CreateHabitCommand( + UserId, "Timezone Habit", null, FrequencyUnit.Day, 1); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + await _habitRepo.Received(1).AddAsync( + Arg.Is(h => h.DueDate == userToday && h.DueDate != utcToday), + Arg.Any()); + } + [Fact] public async Task Handle_WithOptions_PassesDaysAndTimes() { diff --git a/tests/Orbit.Application.Tests/Commands/Habits/DuplicateHabitCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/DuplicateHabitCommandHandlerTests.cs index 2c8d09ba..ecd6cc30 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/DuplicateHabitCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/DuplicateHabitCommandHandlerTests.cs @@ -71,7 +71,7 @@ public async Task Handle_HabitNotFound_ReturnsFailure() [Fact] public async Task Handle_PayGateLimitReached_ReturnsPayGateFailure() { - var original = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1)).Value; + var original = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; SetupAllHabitsForUser(new List { original }); _payGate.CanCreateHabits(Arg.Any(), Arg.Any(), Arg.Any()) @@ -118,10 +118,10 @@ await _habitRepo.Received(1).AddAsync( public async Task Handle_WithChildren_DuplicatesChildHabits() { var parent = Habit.Create(new HabitCreateParams( - UserId, "Morning Routine", FrequencyUnit.Day, 1)).Value; + UserId, "Morning Routine", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var child = Habit.Create(new HabitCreateParams( UserId, "Brush teeth", FrequencyUnit.Day, 1, - ParentHabitId: parent.Id)).Value; + ParentHabitId: parent.Id, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; SetupAllHabitsForUser(new List { parent, child }); @@ -138,10 +138,10 @@ public async Task Handle_WithChildren_DuplicatesChildHabits() public async Task Handle_WithChildren_SubHabitPayGated_ReturnsFailure() { var parent = Habit.Create(new HabitCreateParams( - UserId, "Routine", FrequencyUnit.Day, 1)).Value; + UserId, "Routine", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var child = Habit.Create(new HabitCreateParams( UserId, "Sub", FrequencyUnit.Day, 1, - ParentHabitId: parent.Id)).Value; + ParentHabitId: parent.Id, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; SetupAllHabitsForUser(new List { parent, child }); @@ -159,7 +159,7 @@ public async Task Handle_WithChildren_SubHabitPayGated_ReturnsFailure() [Fact] public async Task Handle_InvalidatesSummaryCache() { - var original = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1)).Value; + var original = Habit.Create(new HabitCreateParams(UserId, "Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; SetupAllHabitsForUser(new List { original }); var realToday = DateOnly.FromDateTime(DateTime.UtcNow); @@ -199,10 +199,10 @@ public async Task Handle_CompletedOneTimeTask_PreservesCompletionOnDuplicate() public async Task Handle_CompletedOneTimeChild_PreservesCompletionOnDuplicatedChild() { var parent = Habit.Create(new HabitCreateParams( - UserId, "Plan trip", FrequencyUnit.Day, 1)).Value; + UserId, "Plan trip", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var child = Habit.Create(new HabitCreateParams( UserId, "Book hotel", null, null, - ParentHabitId: parent.Id)).Value; + ParentHabitId: parent.Id, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; child.Log(new DateOnly(2026, 6, 3)).IsSuccess.Should().BeTrue(); SetupAllHabitsForUser(new List { parent, child }); @@ -223,7 +223,7 @@ public async Task Handle_CompletedOneTimeChild_PreservesCompletionOnDuplicatedCh [Fact] public async Task Handle_UncompletedOneTimeTask_StaysUncompleted() { - var original = Habit.Create(new HabitCreateParams(UserId, "Renew passport", null, null)).Value; + var original = Habit.Create(new HabitCreateParams(UserId, "Renew passport", null, null, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; SetupAllHabitsForUser(new List { original }); var added = CaptureAddedHabits(); diff --git a/tests/Orbit.Application.Tests/Commands/Habits/ReorderHabitsCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/ReorderHabitsCommandHandlerTests.cs index b73ebbad..5302caeb 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/ReorderHabitsCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/ReorderHabitsCommandHandlerTests.cs @@ -26,8 +26,8 @@ public ReorderHabitsCommandHandlerTests() [Fact] public async Task Handle_ValidReorder_UpdatesPositionsAndSaves() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "Habit 1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "Habit 2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; habit1.SetPosition(0); habit2.SetPosition(1); @@ -91,9 +91,9 @@ public async Task Handle_HabitNotFound_ReturnsFailure() [Fact] public async Task Handle_MultipleHabits_LoadsInSingleQuery() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1)).Value; - var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _habitRepo.FindTrackedAsync( Arg.Any>>(), @@ -119,9 +119,9 @@ await _habitRepo.Received(1).FindTrackedAsync( [Fact] public async Task Handle_DuplicatePositions_NormalizesToContiguousSequence() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1)).Value; - var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _habitRepo.FindTrackedAsync( Arg.Any>>(), @@ -147,9 +147,9 @@ public async Task Handle_DuplicatePositions_NormalizesToContiguousSequence() [Fact] public async Task Handle_GappedPositions_NormalizesPreservingOrder() { - var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1)).Value; - var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(UserId, "H1", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(UserId, "H2", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit3 = Habit.Create(new HabitCreateParams(UserId, "H3", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _habitRepo.FindTrackedAsync( Arg.Any>>(), diff --git a/tests/Orbit.Application.Tests/Commands/Referrals/CheckReferralCompletionCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Referrals/CheckReferralCompletionCommandHandlerTests.cs index 03a940b6..ef0c4e23 100644 --- a/tests/Orbit.Application.Tests/Commands/Referrals/CheckReferralCompletionCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Referrals/CheckReferralCompletionCommandHandlerTests.cs @@ -102,7 +102,7 @@ private void SetupHabitsAndLogs(Guid userId, int habitCount, int logCount) var habits = Enumerable.Range(0, habitCount) .Select(_ => { - var habit = Habit.Create(new HabitCreateParams(userId, "Test Habit", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(userId, "Test Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; return habit; }) .ToList(); diff --git a/tests/Orbit.Application.Tests/Queries/Calendar/GetCalendarEventsQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Calendar/GetCalendarEventsQueryHandlerTests.cs index b4500313..18a134e2 100644 --- a/tests/Orbit.Application.Tests/Queries/Calendar/GetCalendarEventsQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Calendar/GetCalendarEventsQueryHandlerTests.cs @@ -156,6 +156,7 @@ public async Task Handle_FiltersOutAlreadyImportedHabitsByGoogleEventId() var importedHabit = Habit.Create(new HabitCreateParams( user.Id, "Existing", Domain.Enums.FrequencyUnit.Week, 1, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), GoogleEventId: "evt_already")).Value; _habitRepo.FindAsync( diff --git a/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs index 63c3aab6..70254ead 100644 --- a/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs @@ -174,7 +174,7 @@ public async Task Handle_SerializedView_ContainsNoPii() private static Habit HabitWithCompletions(Guid userId, string title, DateOnly today, int completions) { - var habit = Habit.Create(new HabitCreateParams(userId, title, FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(userId, title, FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; for (var i = 0; i < completions; i++) habit.Log(today.AddDays(-i), advanceDueDate: false); return habit; diff --git a/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs index ca383569..9027e8ed 100644 --- a/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs @@ -69,7 +69,7 @@ private void StubHabits(params Habit[] habits) private static Habit HabitWithCompletions(Guid userId, string title, int completions) { - var habit = Habit.Create(new HabitCreateParams(userId, title, FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(userId, title, FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; for (var i = 0; i < completions; i++) habit.Log(Today.AddDays(-i), advanceDueDate: false); return habit; diff --git a/tests/Orbit.Domain.Tests/Entities/GoalTests.cs b/tests/Orbit.Domain.Tests/Entities/GoalTests.cs index b5ce6905..ece14694 100644 --- a/tests/Orbit.Domain.Tests/Entities/GoalTests.cs +++ b/tests/Orbit.Domain.Tests/Entities/GoalTests.cs @@ -712,7 +712,7 @@ public void StateTransition_ProgressAutoComplete_ThenReactivate() public void AddHabit_AddsHabit() { var goal = CreateValidGoal(); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit); @@ -723,7 +723,7 @@ public void AddHabit_AddsHabit() public void AddHabit_DuplicateHabit_NoOp() { var goal = CreateValidGoal(); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit); goal.AddHabit(habit); @@ -735,7 +735,7 @@ public void AddHabit_DuplicateHabit_NoOp() public void RemoveHabit_ExistingHabit_Removes() { var goal = CreateValidGoal(); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit); goal.RemoveHabit(habit); @@ -747,7 +747,7 @@ public void RemoveHabit_ExistingHabit_Removes() public void RemoveHabit_NonExistentHabit_NoOp() { var goal = CreateValidGoal(); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.RemoveHabit(habit); @@ -758,7 +758,7 @@ public void RemoveHabit_NonExistentHabit_NoOp() public void RemoveHabit_LastHabitOnStreakGoal_ResetsCurrentValueToZero() { var goal = CreateStreakGoal(targetValue: 7); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Meditate", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Meditate", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit); goal.SyncStreakProgress(5); goal.CurrentValue.Should().Be(5); @@ -774,8 +774,8 @@ public void RemoveHabit_LastHabitOnStreakGoal_ResetsCurrentValueToZero() public void RemoveHabit_NotLastHabitOnStreakGoal_KeepsCurrentValue() { var goal = CreateStreakGoal(targetValue: 7); - var habit1 = Habit.Create(new HabitCreateParams(ValidUserId, "Meditate", FrequencyUnit.Day, 1)).Value; - var habit2 = Habit.Create(new HabitCreateParams(ValidUserId, "Stretch", FrequencyUnit.Day, 1)).Value; + var habit1 = Habit.Create(new HabitCreateParams(ValidUserId, "Meditate", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; + var habit2 = Habit.Create(new HabitCreateParams(ValidUserId, "Stretch", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit1); goal.AddHabit(habit2); goal.SyncStreakProgress(5); @@ -790,7 +790,7 @@ public void RemoveHabit_NotLastHabitOnStreakGoal_KeepsCurrentValue() public void RemoveHabit_LastHabitOnStandardGoal_DoesNotResetCurrentValue() { var goal = CreateValidGoal(targetValue: 100); - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; goal.AddHabit(habit); goal.UpdateProgress(40); diff --git a/tests/Orbit.Domain.Tests/Entities/HabitGoogleEventIdTests.cs b/tests/Orbit.Domain.Tests/Entities/HabitGoogleEventIdTests.cs index 3b2d0e7f..39e5bb5e 100644 --- a/tests/Orbit.Domain.Tests/Entities/HabitGoogleEventIdTests.cs +++ b/tests/Orbit.Domain.Tests/Entities/HabitGoogleEventIdTests.cs @@ -11,7 +11,7 @@ public void Create_WithGoogleEventId_AssignsProperty() { var result = Habit.Create(new HabitCreateParams( Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1, - GoogleEventId: "evt_123")); + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), GoogleEventId: "evt_123")); result.IsSuccess.Should().BeTrue(); result.Value.GoogleEventId.Should().Be("evt_123"); @@ -21,7 +21,7 @@ public void Create_WithGoogleEventId_AssignsProperty() public void Create_WithoutGoogleEventId_IsNull() { var result = Habit.Create(new HabitCreateParams( - Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1)); + Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.Value.GoogleEventId.Should().BeNull(); } @@ -30,7 +30,7 @@ public void Create_WithoutGoogleEventId_IsNull() public void SetGoogleEventId_UpdatesProperty() { var habit = Habit.Create(new HabitCreateParams( - Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1)).Value; + Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; habit.SetGoogleEventId("evt_456"); @@ -42,7 +42,7 @@ public void SetGoogleEventId_CanClearId() { var habit = Habit.Create(new HabitCreateParams( Guid.NewGuid(), "Standup", FrequencyUnit.Day, 1, - GoogleEventId: "evt_123")).Value; + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), GoogleEventId: "evt_123")).Value; habit.SetGoogleEventId(null); diff --git a/tests/Orbit.Domain.Tests/Entities/HabitTests.cs b/tests/Orbit.Domain.Tests/Entities/HabitTests.cs index 388ef032..0bdf7c2f 100644 --- a/tests/Orbit.Domain.Tests/Entities/HabitTests.cs +++ b/tests/Orbit.Domain.Tests/Entities/HabitTests.cs @@ -29,7 +29,7 @@ private static Habit CreateValidHabit( Description: "Daily workout", Days: days, IsBadHabit: isBadHabit, - DueDate: dueDate, + DueDate: dueDate ?? DateOnly.FromDateTime(DateTime.UtcNow), ParentHabitId: parentHabitId)); return result.Value; } @@ -41,7 +41,7 @@ private static Habit CreateOneTimeHabit(DateOnly? dueDate = null) "One-time task", FrequencyUnit: null, FrequencyQuantity: null, - DueDate: dueDate)).Value; + DueDate: dueDate ?? DateOnly.FromDateTime(DateTime.UtcNow))).Value; } [Fact] @@ -82,7 +82,7 @@ public void Restore_ClearsDeletedState() [Fact] public void Create_ValidInput_ReturnsSuccess() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsSuccess.Should().BeTrue(); result.Value.Title.Should().Be("Exercise"); @@ -93,7 +93,7 @@ public void Create_ValidInput_ReturnsSuccess() [Fact] public void Create_EmptyUserId_ReturnsFailure() { - var result = Habit.Create(new HabitCreateParams(Guid.Empty, "Exercise", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(Guid.Empty, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("User ID is required"); @@ -102,7 +102,7 @@ public void Create_EmptyUserId_ReturnsFailure() [Fact] public void Create_EmptyTitle_ReturnsFailure() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Title is required"); @@ -111,7 +111,7 @@ public void Create_EmptyTitle_ReturnsFailure() [Fact] public void Create_NegativeFrequencyQty_ReturnsFailure() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, -1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, -1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Frequency quantity must be greater than 0"); @@ -120,7 +120,7 @@ public void Create_NegativeFrequencyQty_ReturnsFailure() [Fact] public void Create_ZeroFrequencyQty_ReturnsFailure() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 0)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 0, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Frequency quantity must be greater than 0"); @@ -131,7 +131,7 @@ public void Create_DaysWithQtyGreaterThan1_ReturnsFailure() { var days = new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }; - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Week, 2, Days: days)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Week, 2, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), Days: days)); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Days can only be set when frequency quantity is 1"); @@ -142,7 +142,7 @@ public void Create_DaysWithWeeklyFrequency_ReturnsFailure() { var days = new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }; - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Week, 1, Days: days)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Week, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), Days: days)); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Days can only be set when frequency quantity is 1"); @@ -153,25 +153,15 @@ public void Create_DaysWithDailyFrequency_Succeeds() { var days = new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }; - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, Days: days)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), Days: days)); result.IsSuccess.Should().BeTrue(); } - [Fact] - public void Create_NullDueDate_DefaultsToToday() - { - var today = DateOnly.FromDateTime(DateTime.UtcNow); - - var habit = CreateValidHabit(dueDate: null); - - habit.DueDate.Should().Be(today); - } - [Fact] public void Create_TrimsTitle() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, " Exercise ", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, " Exercise ", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.Value.Title.Should().Be("Exercise"); } @@ -647,6 +637,7 @@ public void Create_Flexible_WithFrequency_ReturnsSuccess() "Exercise", FrequencyUnit.Week, 3, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsFlexible: true)); result.IsSuccess.Should().BeTrue(); @@ -663,6 +654,7 @@ public void Create_Flexible_WithoutFrequencyUnit_ReturnsFailure() "Exercise", FrequencyUnit: null, FrequencyQuantity: null, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsFlexible: true)); result.IsFailure.Should().BeTrue(); @@ -679,6 +671,7 @@ public void Create_Flexible_WithDays_ReturnsFailure() "Exercise", FrequencyUnit.Week, 3, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), Days: days, IsFlexible: true)); @@ -689,7 +682,7 @@ public void Create_Flexible_WithDays_ReturnsFailure() [Fact] public void Create_Flexible_DefaultsFalse() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsSuccess.Should().BeTrue(); result.Value.IsFlexible.Should().BeFalse(); @@ -780,6 +773,7 @@ public void Update_FlexibleWithDays_ReturnsFailure() "Exercise", FrequencyUnit.Week, 3, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsFlexible: true)); var habit = result.Value; @@ -805,6 +799,7 @@ public void Update_FlexibleWithoutFrequency_ReturnsFailure() "Exercise", FrequencyUnit.Week, 3, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsFlexible: true)); var habit = result.Value; @@ -831,7 +826,7 @@ public void Create_WithValidScheduledReminders_ReturnsSuccess() new(ScheduledReminderWhen.SameDay, new TimeOnly(9, 0)) }; - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), ScheduledReminders: reminders)); result.IsSuccess.Should().BeTrue(); @@ -841,7 +836,7 @@ public void Create_WithValidScheduledReminders_ReturnsSuccess() [Fact] public void Create_WithEmptyScheduledReminders_DefaultsToEmpty() { - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1)); + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))); result.IsSuccess.Should().BeTrue(); result.Value.ScheduledReminders.Should().BeEmpty(); @@ -854,7 +849,7 @@ public void Create_ScheduledReminders_OverLimit_ReturnsFailure() .Select(i => new ScheduledReminderTime(ScheduledReminderWhen.SameDay, new TimeOnly(8 + i, 0))) .ToList(); - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), ScheduledReminders: reminders)); result.IsFailure.Should().BeTrue(); @@ -870,7 +865,7 @@ public void Create_ScheduledReminders_Duplicates_ReturnsFailure() new(ScheduledReminderWhen.SameDay, new TimeOnly(9, 0)) }; - var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, + var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), ScheduledReminders: reminders)); result.IsFailure.Should().BeTrue(); @@ -916,7 +911,7 @@ public void Update_ScheduledReminders_Null_KeepsExisting() { new(ScheduledReminderWhen.SameDay, new TimeOnly(9, 0)) }; - var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, + var habit = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), ScheduledReminders: reminders)).Value; var result = habit.Update(new HabitUpdateParams("Exercise", null, FrequencyUnit.Day, 1, null, false, null, @@ -1349,7 +1344,7 @@ public void UpdateChecklist_ReplacesItems() public void Create_GeneralHabit_Success() { var result = Habit.Create(new HabitCreateParams( - ValidUserId, "General", null, null, IsGeneral: true)); + ValidUserId, "General", null, null, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsGeneral: true)); result.IsSuccess.Should().BeTrue(); result.Value.IsGeneral.Should().BeTrue(); @@ -1359,7 +1354,7 @@ public void Create_GeneralHabit_Success() public void Create_GeneralWithFrequency_ReturnsFailure() { var result = Habit.Create(new HabitCreateParams( - ValidUserId, "General", FrequencyUnit.Day, 1, IsGeneral: true)); + ValidUserId, "General", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsGeneral: true)); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("General habits cannot have a frequency"); @@ -1369,7 +1364,7 @@ public void Create_GeneralWithFrequency_ReturnsFailure() public void Create_GeneralBadHabit_ReturnsFailure() { var result = Habit.Create(new HabitCreateParams( - ValidUserId, "General Bad", null, null, IsGeneral: true, IsBadHabit: true)); + ValidUserId, "General Bad", null, null, DueDate: DateOnly.FromDateTime(DateTime.UtcNow), IsGeneral: true, IsBadHabit: true)); result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("General habits cannot be bad habits"); @@ -1380,6 +1375,7 @@ public void Create_DueEndTimeBeforeDueTime_ReturnsFailure() { var result = Habit.Create(new HabitCreateParams( ValidUserId, "Exercise", FrequencyUnit.Day, 1, + DueDate: DateOnly.FromDateTime(DateTime.UtcNow), DueTime: new TimeOnly(10, 0), DueEndTime: new TimeOnly(9, 0))); result.IsFailure.Should().BeTrue(); @@ -1425,4 +1421,31 @@ public void Unlog_FlexibleHabit_SoftDeletesLogButDoesNotResetDueDate() habit.Logs.Should().NotContain(l => !l.IsDeleted); habit.DueDate.Should().Be(originalDueDate); } + + [Fact] + public void Create_UsesProvidedDueDateVerbatim_WithoutUtcSubstitution() + { + var utcToday = DateOnly.FromDateTime(DateTime.UtcNow); + var userToday = utcToday.AddDays(1); + + var result = Habit.Create(new HabitCreateParams( + ValidUserId, "Meditate", FrequencyUnit.Day, 1, DueDate: userToday)); + + result.IsSuccess.Should().BeTrue(); + result.Value.DueDate.Should().Be(userToday); + result.Value.DueDate.Should().NotBe(utcToday); + } + + [Fact] + public void Create_MonthlyHabit_AnchorsOriginalDayToProvidedDueDate() + { + var providedDueDate = new DateOnly(2026, 1, 31); + + var result = Habit.Create(new HabitCreateParams( + ValidUserId, "Pay rent", FrequencyUnit.Month, 1, DueDate: providedDueDate)); + + result.IsSuccess.Should().BeTrue(); + result.Value.DueDate.Should().Be(providedDueDate); + result.Value.OriginalDayOfMonth.Should().Be(31); + } } diff --git a/tests/Orbit.Infrastructure.Tests/Persistence/AccountResetRepositoryTests.cs b/tests/Orbit.Infrastructure.Tests/Persistence/AccountResetRepositoryTests.cs index 7477c3ed..8937a580 100644 --- a/tests/Orbit.Infrastructure.Tests/Persistence/AccountResetRepositoryTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Persistence/AccountResetRepositoryTests.cs @@ -130,7 +130,7 @@ private async Task SeedOrphanProneDataForUser(Guid userId) _dbContext.Goals.Add(goal); var habit = Habit.Create(new HabitCreateParams( - userId, "Habit", FrequencyUnit.Day, 1)).Value; + userId, "Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var today = DateOnly.FromDateTime(DateTime.UtcNow); habit.Log(today, "note"); habit.Log(today.AddDays(-1), "note"); @@ -225,7 +225,7 @@ public async Task DeleteAllUserDataAsync_RemovesSocialGraphForUser_LeavesUnrelat SeedUser(unrelatedCounterpartId, "unrelated-counterpart@example.com"); var cheeredHabit = Habit.Create(new HabitCreateParams( - _otherUserId, "Habit", FrequencyUnit.Day, 1)).Value; + _otherUserId, "Habit", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; _dbContext.Habits.Add(cheeredHabit); SeedSocialDataForUser(_userId, _otherUserId, cheeredHabit.Id); diff --git a/tests/Orbit.Infrastructure.Tests/Services/SystemPromptBuilderTests.cs b/tests/Orbit.Infrastructure.Tests/Services/SystemPromptBuilderTests.cs index 47d96189..77a300df 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/SystemPromptBuilderTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/SystemPromptBuilderTests.cs @@ -35,7 +35,7 @@ public void Build_NoHabits_ContainsNoneMarker() [Fact] public void Build_WithHabits_ListsHabitTitle() { - var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var habits = new[] { habit }; var facts = Array.Empty(); @@ -47,7 +47,7 @@ public void Build_WithHabits_ListsHabitTitle() [Fact] public void Build_WithMetrics_IncludesStreakInfo() { - var habit = Habit.Create(new HabitCreateParams(TestUserId, "Meditation", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(TestUserId, "Meditation", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var habits = new[] { habit }; var facts = Array.Empty(); var metrics = new Dictionary @@ -199,7 +199,7 @@ public void Build_WithFactContainingControlCharacters_SanitizesPromptData() public void BuildStatic_IsRequestInvariant_AndExcludesDynamicHabitIndex() { ISystemPromptBuilder builder = new SystemPromptBuilder(); - var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var withHabit = new PromptBuildRequest([habit], Array.Empty()); var empty = new PromptBuildRequest(Array.Empty(), Array.Empty()); @@ -215,7 +215,7 @@ public void BuildStatic_IsRequestInvariant_AndExcludesDynamicHabitIndex() public void BuildDynamic_ContainsUserData_AndExcludesStaticRules() { ISystemPromptBuilder builder = new SystemPromptBuilder(); - var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1)).Value; + var habit = Habit.Create(new HabitCreateParams(TestUserId, "Morning Run", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow))).Value; var request = new PromptBuildRequest([habit], Array.Empty()); var dynamicPrompt = builder.BuildDynamic(request);