diff --git a/src/Orbit.Api/Controllers/HabitsController.cs b/src/Orbit.Api/Controllers/HabitsController.cs index 407d3f77..4f020dda 100644 --- a/src/Orbit.Api/Controllers/HabitsController.cs +++ b/src/Orbit.Api/Controllers/HabitsController.cs @@ -205,6 +205,19 @@ public async Task LogHabit( : BadRequest(new { error = result.Error }); } + [HttpPost("{id:guid}/skip")] + public async Task SkipHabit( + Guid id, + CancellationToken cancellationToken) + { + var command = new SkipHabitCommand(HttpContext.GetUserId(), id); + var result = await mediator.Send(command, cancellationToken); + + return result.IsSuccess + ? NoContent() + : BadRequest(new { error = result.Error }); + } + [HttpPut("{id:guid}")] public async Task UpdateHabit( Guid id, diff --git a/src/Orbit.Application/Habits/Commands/SkipHabitCommand.cs b/src/Orbit.Application/Habits/Commands/SkipHabitCommand.cs new file mode 100644 index 00000000..f5f4fc1f --- /dev/null +++ b/src/Orbit.Application/Habits/Commands/SkipHabitCommand.cs @@ -0,0 +1,49 @@ +using MediatR; +using Microsoft.Extensions.Caching.Memory; +using Orbit.Application.Common; +using Orbit.Domain.Common; +using Orbit.Domain.Entities; +using Orbit.Domain.Interfaces; + +namespace Orbit.Application.Habits.Commands; + +public record SkipHabitCommand(Guid UserId, Guid HabitId) : IRequest; + +public class SkipHabitCommandHandler( + IGenericRepository habitRepository, + IUserDateService userDateService, + IUnitOfWork unitOfWork, + IMemoryCache cache) : IRequestHandler +{ + public async Task Handle(SkipHabitCommand request, CancellationToken cancellationToken) + { + var habit = await habitRepository.FindOneTrackedAsync( + h => h.Id == request.HabitId, + cancellationToken: cancellationToken); + + if (habit is null) + return Result.Failure(ErrorMessages.HabitNotFound); + + if (habit.UserId != request.UserId) + return Result.Failure(ErrorMessages.HabitNotOwned); + + if (habit.IsCompleted) + return Result.Failure("Cannot skip a completed habit."); + + if (habit.FrequencyUnit is null) + return Result.Failure("Cannot skip a one-time task."); + + var today = await userDateService.GetUserTodayAsync(request.UserId, cancellationToken); + + if (habit.DueDate > today) + return Result.Failure("Cannot skip a habit that is not yet due."); + + habit.AdvanceDueDate(today); + + await unitOfWork.SaveChangesAsync(cancellationToken); + + CacheInvalidationHelper.InvalidateSummaryCache(cache, habit.UserId); + + return Result.Success(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/SkipHabitCommandValidator.cs b/src/Orbit.Application/Habits/Validators/SkipHabitCommandValidator.cs new file mode 100644 index 00000000..bd55bb97 --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/SkipHabitCommandValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Orbit.Application.Habits.Commands; + +namespace Orbit.Application.Habits.Validators; + +public class SkipHabitCommandValidator : AbstractValidator +{ + public SkipHabitCommandValidator() + { + RuleFor(x => x.UserId) + .NotEmpty(); + + RuleFor(x => x.HabitId) + .NotEmpty(); + } +}