using Microsoft.EntityFrameworkCore; using Purrse.Core.DTOs; using Purrse.Core.Enums; using Purrse.Core.Interfaces.Services; using Purrse.Core.Models; using Purrse.Data; namespace Purrse.Api.Services; public class ScheduledTransactionService : IScheduledTransactionService { private readonly PurrseDbContext _db; private readonly ITransactionService _transactionService; public ScheduledTransactionService(PurrseDbContext db, ITransactionService transactionService) { _db = db; _transactionService = transactionService; } public async Task> GetAllAsync(Guid userId) { return await _db.ScheduledTransactions .Include(s => s.Account).Include(s => s.Category) .Where(s => s.UserId == userId) .OrderBy(s => s.NextDueDate) .Select(s => MapToResponse(s)) .ToListAsync(); } public async Task GetByIdAsync(Guid userId, Guid id) { var s = await _db.ScheduledTransactions.Include(s => s.Account).Include(s => s.Category) .FirstOrDefaultAsync(s => s.Id == id && s.UserId == userId); return s == null ? null : MapToResponse(s); } public async Task CreateAsync(Guid userId, CreateScheduledTransactionRequest request) { var scheduled = new ScheduledTransaction { Id = Guid.NewGuid(), UserId = userId, AccountId = request.AccountId, Amount = request.Amount, PayeeName = request.PayeeName, PayeeId = request.PayeeId, CategoryId = request.CategoryId, Memo = request.Memo, Type = request.Type, Frequency = request.Frequency, NextDueDate = request.NextDueDate, EndDate = request.EndDate, AutoPost = request.AutoPost, ReminderDaysBefore = request.ReminderDaysBefore }; _db.ScheduledTransactions.Add(scheduled); await _db.SaveChangesAsync(); scheduled = await _db.ScheduledTransactions.Include(s => s.Account).Include(s => s.Category) .FirstAsync(s => s.Id == scheduled.Id); return MapToResponse(scheduled); } public async Task UpdateAsync(Guid userId, Guid id, UpdateScheduledTransactionRequest request) { var s = await _db.ScheduledTransactions.Include(s => s.Account).Include(s => s.Category) .FirstOrDefaultAsync(s => s.Id == id && s.UserId == userId) ?? throw new KeyNotFoundException("Scheduled transaction not found"); s.Amount = request.Amount; s.PayeeName = request.PayeeName; s.PayeeId = request.PayeeId; s.CategoryId = request.CategoryId; s.Memo = request.Memo; s.Type = request.Type; s.Frequency = request.Frequency; s.NextDueDate = request.NextDueDate; s.EndDate = request.EndDate; s.AutoPost = request.AutoPost; s.ReminderDaysBefore = request.ReminderDaysBefore; s.IsActive = request.IsActive; await _db.SaveChangesAsync(); return MapToResponse(s); } public async Task DeleteAsync(Guid userId, Guid id) { var s = await _db.ScheduledTransactions.FirstOrDefaultAsync(s => s.Id == id && s.UserId == userId) ?? throw new KeyNotFoundException("Scheduled transaction not found"); _db.ScheduledTransactions.Remove(s); await _db.SaveChangesAsync(); } public async Task SkipAsync(Guid userId, Guid id) { var s = await _db.ScheduledTransactions.FirstOrDefaultAsync(s => s.Id == id && s.UserId == userId) ?? throw new KeyNotFoundException("Scheduled transaction not found"); s.NextDueDate = CalculateNextDate(s.NextDueDate, s.Frequency); if (s.EndDate.HasValue && s.NextDueDate > s.EndDate.Value) s.IsActive = false; await _db.SaveChangesAsync(); } public async Task PostNowAsync(Guid userId, Guid id) { var s = await _db.ScheduledTransactions.FirstOrDefaultAsync(s => s.Id == id && s.UserId == userId) ?? throw new KeyNotFoundException("Scheduled transaction not found"); var result = await _transactionService.CreateAsync(userId, new CreateTransactionRequest( s.AccountId, s.NextDueDate, s.Amount, s.PayeeName, s.PayeeId, s.CategoryId, s.Memo, null, null, s.Type, null)); s.NextDueDate = CalculateNextDate(s.NextDueDate, s.Frequency); if (s.EndDate.HasValue && s.NextDueDate > s.EndDate.Value) s.IsActive = false; await _db.SaveChangesAsync(); return result; } public async Task ProcessDueTransactionsAsync() { var due = await _db.ScheduledTransactions .Where(s => s.IsActive && s.AutoPost && s.NextDueDate <= DateTime.UtcNow.Date) .ToListAsync(); foreach (var s in due) { await PostNowAsync(s.UserId, s.Id); } } private static DateTime CalculateNextDate(DateTime current, Frequency frequency) => frequency switch { Frequency.Daily => current.AddDays(1), Frequency.Weekly => current.AddDays(7), Frequency.BiWeekly => current.AddDays(14), Frequency.SemiMonthly => current.Day <= 15 ? new DateTime(current.Year, current.Month, Math.Min(28, DateTime.DaysInMonth(current.Year, current.Month)), 0, 0, 0, DateTimeKind.Utc) : current.AddMonths(1).AddDays(-(current.Day - 1)), Frequency.Monthly => current.AddMonths(1), Frequency.Quarterly => current.AddMonths(3), Frequency.SemiAnnually => current.AddMonths(6), Frequency.Annually => current.AddYears(1), _ => current.AddMonths(1) }; private static ScheduledTransactionResponse MapToResponse(ScheduledTransaction s) => new( s.Id, s.AccountId, s.Account?.Name ?? "", s.Amount, s.PayeeName, s.CategoryId, s.Category?.Name, s.Memo, s.Type, s.Frequency, s.NextDueDate, s.EndDate, s.AutoPost, s.ReminderDaysBefore, s.IsActive); }