diff --git a/src/Orbit.Application/Auth/Commands/SendCodeCommand.cs b/src/Orbit.Application/Auth/Commands/SendCodeCommand.cs index 2ab89b90..96abfe6f 100644 --- a/src/Orbit.Application/Auth/Commands/SendCodeCommand.cs +++ b/src/Orbit.Application/Auth/Commands/SendCodeCommand.cs @@ -19,6 +19,19 @@ public async Task Handle(SendCodeCommand request, CancellationToken canc var email = request.Email.Trim().ToLowerInvariant(); var cacheKey = $"verify:{email}"; + // Reviewer test account: skip email, store fixed code + var reviewerEmail = Environment.GetEnvironmentVariable("REVIEWER_TEST_EMAIL")?.ToLowerInvariant(); + var reviewerCode = Environment.GetEnvironmentVariable("REVIEWER_TEST_CODE"); + if (reviewerEmail is not null && reviewerCode is not null && email == reviewerEmail) + { + var testEntry = new VerificationEntry(reviewerCode, 0, DateTime.UtcNow); + cache.Set(cacheKey, testEntry, new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) + }); + return Result.Success(); + } + // Rate limit: no new code within 60 seconds if (cache.TryGetValue(cacheKey, out VerificationEntry? existing) && existing is not null) { diff --git a/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs b/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs index e25a3b3d..8451e25f 100644 --- a/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs +++ b/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs @@ -62,10 +62,25 @@ public record GetHabitScheduleQuery( int PageSize = 50) : IRequest>; public class GetHabitScheduleQueryHandler( - IGenericRepository habitRepository) : IRequestHandler> + IGenericRepository habitRepository, + IUserDateService userDateService, + IUnitOfWork unitOfWork) : IRequestHandler> { public async Task> Handle(GetHabitScheduleQuery request, CancellationToken cancellationToken) { + // Advance stale bad habit DueDates so they show on the correct next scheduled day + var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); + var staleBadHabits = await habitRepository.FindTrackedAsync( + h => h.UserId == request.UserId && h.IsBadHabit && h.FrequencyUnit != null && h.DueDate < today, + cancellationToken); + + if (staleBadHabits.Count > 0) + { + foreach (var habit in staleBadHabits) + habit.AdvanceDueDate(today); + await unitOfWork.SaveChangesAsync(cancellationToken); + } + var allHabits = await habitRepository.FindAsync( h => h.UserId == request.UserId, q => q.Include(h => h.Tags), @@ -127,7 +142,7 @@ bool HasDescendantWithTag(Guid parentId) var scheduledDates = HabitScheduleService.GetScheduledDates(habit, request.DateFrom, request.DateTo); var isOverdue = false; - if (request.IncludeOverdue && !habit.IsCompleted && habit.DueDate < request.DateFrom) + if (request.IncludeOverdue && !habit.IsCompleted && !habit.IsBadHabit && habit.DueDate < request.DateFrom) { isOverdue = true; } @@ -193,7 +208,7 @@ private static bool HasAnyDescendantDue(Guid parentId, ILookup loo { if (HabitScheduleService.GetScheduledDates(child, dateFrom, dateTo).Count > 0) return true; - if (!child.IsCompleted && child.DueDate < dateFrom) + if (!child.IsCompleted && !child.IsBadHabit && child.DueDate < dateFrom) return true; if (HasAnyDescendantDue(child.Id, lookup, dateFrom, dateTo)) return true; @@ -205,7 +220,7 @@ private static List MapChildren(Guid parentId, ILookup HabitScheduleService.GetScheduledDates(c, dateFrom, dateTo).Count > 0 || c.IsCompleted - || (!c.IsCompleted && c.DueDate < dateFrom) + || (!c.IsCompleted && !c.IsBadHabit && c.DueDate < dateFrom) || HasAnyDescendantDue(c.Id, lookup, dateFrom, dateTo)) .OrderBy(c => c.Position ?? int.MaxValue) .ThenBy(c => c.CreatedAtUtc) diff --git a/src/Orbit.Domain/Entities/Habit.cs b/src/Orbit.Domain/Entities/Habit.cs index 7d197333..e9c2fab4 100644 --- a/src/Orbit.Domain/Entities/Habit.cs +++ b/src/Orbit.Domain/Entities/Habit.cs @@ -113,7 +113,7 @@ public Result Log(DateOnly date, string? note = null) return Result.Success(log); } - private void AdvanceDueDate(DateOnly today) + public void AdvanceDueDate(DateOnly today) { do { diff --git a/src/Orbit.Domain/Interfaces/IGenericRepository.cs b/src/Orbit.Domain/Interfaces/IGenericRepository.cs index 6d571a2d..ef387f59 100644 --- a/src/Orbit.Domain/Interfaces/IGenericRepository.cs +++ b/src/Orbit.Domain/Interfaces/IGenericRepository.cs @@ -10,6 +10,7 @@ public interface IGenericRepository where T : Entity Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); Task> FindAsync(Expression> predicate, Func, IQueryable>? includes, CancellationToken cancellationToken = default); Task FindOneTrackedAsync(Expression> predicate, Func, IQueryable>? includes = null, CancellationToken cancellationToken = default); + Task> FindTrackedAsync(Expression> predicate, CancellationToken cancellationToken = default); Task AddAsync(T entity, CancellationToken cancellationToken = default); void Update(T entity); void Remove(T entity); diff --git a/src/Orbit.Infrastructure/Persistence/GenericRepository.cs b/src/Orbit.Infrastructure/Persistence/GenericRepository.cs index bf5aae26..6254805b 100644 --- a/src/Orbit.Infrastructure/Persistence/GenericRepository.cs +++ b/src/Orbit.Infrastructure/Persistence/GenericRepository.cs @@ -48,6 +48,13 @@ public async Task> FindAsync( return await query.FirstOrDefaultAsync(predicate, cancellationToken); } + public async Task> FindTrackedAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await _dbSet.Where(predicate).ToListAsync(cancellationToken); + } + public async Task AddAsync(T entity, CancellationToken cancellationToken = default) { await _dbSet.AddAsync(entity, cancellationToken);