Refactor. Add data layer
This commit is contained in:
1 parent
3b7e4e6ec5
commit
b6b794f0b4
82 files changed
+2930
-2811
No files matched your search
@@ -0,0 +1,287 @@
|
||||
using BudgetApp.Data.Entities;
|
||||
using BudgetApp.Data.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BudgetApp.Data.Repositories;
|
||||
|
||||
public class TransactionRepository(ApplicationDbContext db) : ITransactionRepository
|
||||
{
|
||||
public async Task<List<Transaction>> GetTransactionsAsync(string userId)
|
||||
{
|
||||
return await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.Include(t => t.Category)
|
||||
.Include(t => t.ParentTransaction)
|
||||
.Where(t => t.Account.UserId == userId)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Transaction>> GetRecentTransactionsAsync(string userId, int count = 5)
|
||||
{
|
||||
return await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.Include(t => t.Category)
|
||||
.Include(t => t.ParentTransaction)
|
||||
.Where(t => t.Account.UserId == userId && t.Date <= DateTime.Today)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Transaction>> GetUpcomingTransactionsAsync(string userId, int count = 5)
|
||||
{
|
||||
return await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.Include(t => t.Category)
|
||||
.Include(t => t.ParentTransaction)
|
||||
.Where(t => t.Account.UserId == userId && t.Date > DateTime.Today)
|
||||
.OrderBy(t => t.Date)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Transaction>> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to)
|
||||
{
|
||||
return await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.Include(t => t.Category)
|
||||
.Include(t => t.ParentTransaction)
|
||||
.Where(t => t.Account.UserId == userId && t.Date.Date >= from.Date && t.Date.Date <= to.Date)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Transaction>> GetTransactionsByAccountAsync(int accountId, string userId)
|
||||
{
|
||||
return await db.Transactions
|
||||
.Include(t => t.Category)
|
||||
.Where(t => t.AccountId == accountId && t.Account.UserId == userId)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task CreateTransactionAsync(Transaction transaction)
|
||||
{
|
||||
// For recurring transactions: enforce max 1 year end date
|
||||
if (transaction.RecurrenceType != RecurrenceType.None)
|
||||
{
|
||||
var maxEnd = transaction.Date.AddYears(1);
|
||||
if (transaction.RecurrenceEndDate is null || transaction.RecurrenceEndDate.Value > maxEnd)
|
||||
{
|
||||
transaction.RecurrenceEndDate = maxEnd;
|
||||
}
|
||||
}
|
||||
|
||||
db.Transactions.Add(transaction);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Immediately materialize child occurrences for recurring transactions
|
||||
if (transaction.RecurrenceType != RecurrenceType.None)
|
||||
{
|
||||
var endDate = transaction.RecurrenceEndDate ?? transaction.Date.AddYears(1);
|
||||
var newTransactions = new List<Transaction>();
|
||||
var nextDate = GetNextOccurrence(transaction.Date, transaction.RecurrenceType);
|
||||
|
||||
while (nextDate <= endDate)
|
||||
{
|
||||
newTransactions.Add(new Transaction
|
||||
{
|
||||
Description = transaction.Description,
|
||||
Amount = transaction.Amount,
|
||||
Date = nextDate,
|
||||
Type = transaction.Type,
|
||||
AccountId = transaction.AccountId,
|
||||
CategoryId = transaction.CategoryId,
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
ParentTransactionId = transaction.Id
|
||||
});
|
||||
nextDate = GetNextOccurrence(nextDate, transaction.RecurrenceType);
|
||||
}
|
||||
|
||||
if (newTransactions.Count > 0)
|
||||
{
|
||||
db.Transactions.AddRange(newTransactions);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateTransactionAsync(int id, string userId, string description, decimal amount,
|
||||
DateTime date, TransactionType type, int accountId, int? categoryId,
|
||||
RecurrenceType recurrenceType = RecurrenceType.None, DateTime? recurrenceEndDate = null)
|
||||
{
|
||||
var transaction = await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
|
||||
|
||||
if (transaction is null) return;
|
||||
|
||||
var isPartOfSeries = transaction.RecurrenceType != RecurrenceType.None
|
||||
|| transaction.ParentTransactionId is not null;
|
||||
|
||||
transaction.Description = description;
|
||||
transaction.Amount = amount;
|
||||
transaction.Date = date;
|
||||
transaction.Type = type;
|
||||
transaction.AccountId = accountId;
|
||||
transaction.CategoryId = categoryId;
|
||||
|
||||
if (isPartOfSeries)
|
||||
{
|
||||
var parentId = transaction.ParentTransactionId ?? transaction.Id;
|
||||
var cutoffDate = transaction.Date.Date;
|
||||
|
||||
var futureSiblings = await db.Transactions
|
||||
.Where(t => (t.Id == parentId || t.ParentTransactionId == parentId)
|
||||
&& t.Id != id
|
||||
&& t.Date.Date >= cutoffDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var sibling in futureSiblings)
|
||||
{
|
||||
sibling.Description = description;
|
||||
sibling.Amount = amount;
|
||||
sibling.Type = type;
|
||||
sibling.AccountId = accountId;
|
||||
sibling.CategoryId = categoryId;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
transaction.RecurrenceType = recurrenceType;
|
||||
transaction.RecurrenceEndDate = recurrenceEndDate;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteTransactionAsync(int id, string userId)
|
||||
{
|
||||
var transaction = await db.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
|
||||
|
||||
if (transaction is not null)
|
||||
{
|
||||
db.Transactions.Remove(transaction);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteTransactionSeriesAsync(int id, string userId)
|
||||
{
|
||||
var transaction = await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
|
||||
|
||||
if (transaction is null) return;
|
||||
|
||||
var parentId = transaction.ParentTransactionId ?? transaction.Id;
|
||||
var cutoffDate = transaction.Date.Date;
|
||||
|
||||
var allChildren = await db.Transactions
|
||||
.Where(t => t.ParentTransactionId == parentId)
|
||||
.ToListAsync();
|
||||
|
||||
var toDelete = allChildren.Where(c => c.Date.Date >= cutoffDate).ToList();
|
||||
var toKeep = allChildren.Where(c => c.Date.Date < cutoffDate).ToList();
|
||||
|
||||
db.Transactions.RemoveRange(toDelete);
|
||||
|
||||
var parent = parentId == transaction.Id
|
||||
? transaction
|
||||
: await db.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == parentId && t.Account.UserId == userId);
|
||||
|
||||
if (parent is not null)
|
||||
{
|
||||
if (parent.Date.Date >= cutoffDate)
|
||||
{
|
||||
foreach (var child in toKeep)
|
||||
{
|
||||
child.ParentTransactionId = null;
|
||||
}
|
||||
db.Transactions.Remove(parent);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.RecurrenceEndDate = cutoffDate.AddDays(-1);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true)
|
||||
{
|
||||
var transaction = await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
|
||||
|
||||
if (transaction is null) return;
|
||||
|
||||
transaction.IsPaid = isPaid;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null)
|
||||
{
|
||||
var recurringParents = await db.Transactions
|
||||
.Include(t => t.Account)
|
||||
.Where(t => t.Account.UserId == userId
|
||||
&& t.RecurrenceType != RecurrenceType.None
|
||||
&& t.ParentTransactionId == null)
|
||||
.ToListAsync();
|
||||
|
||||
var defaultHorizon = DateTime.Today.AddDays(60);
|
||||
var horizon = windowEnd.HasValue && windowEnd.Value > defaultHorizon ? windowEnd.Value : defaultHorizon;
|
||||
var newTransactions = new List<Transaction>();
|
||||
|
||||
foreach (var parent in recurringParents)
|
||||
{
|
||||
var endDate = parent.RecurrenceEndDate.HasValue && parent.RecurrenceEndDate.Value < horizon
|
||||
? parent.RecurrenceEndDate.Value
|
||||
: horizon;
|
||||
|
||||
var latestChildDate = await db.Transactions
|
||||
.Where(t => t.ParentTransactionId == parent.Id)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.Select(t => (DateTime?)t.Date)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var lastDate = latestChildDate ?? parent.Date;
|
||||
var nextDate = GetNextOccurrence(lastDate, parent.RecurrenceType);
|
||||
|
||||
while (nextDate <= endDate)
|
||||
{
|
||||
newTransactions.Add(new Transaction
|
||||
{
|
||||
Description = parent.Description,
|
||||
Amount = parent.Amount,
|
||||
Date = nextDate,
|
||||
Type = parent.Type,
|
||||
AccountId = parent.AccountId,
|
||||
CategoryId = parent.CategoryId,
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
ParentTransactionId = parent.Id
|
||||
});
|
||||
nextDate = GetNextOccurrence(nextDate, parent.RecurrenceType);
|
||||
}
|
||||
}
|
||||
|
||||
if (newTransactions.Count > 0)
|
||||
{
|
||||
db.Transactions.AddRange(newTransactions);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
internal static DateTime GetNextOccurrence(DateTime current, RecurrenceType recurrence) => recurrence switch
|
||||
{
|
||||
RecurrenceType.Daily => current.AddDays(1),
|
||||
RecurrenceType.Weekly => current.AddDays(7),
|
||||
RecurrenceType.Monthly => current.AddMonths(1),
|
||||
RecurrenceType.Yearly => current.AddYears(1),
|
||||
_ => DateTime.MaxValue
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user