From b6b794f0b4898b96c6351274e7315eb8bb85ebdb Mon Sep 17 00:00:00 2001 From: Ross Miller Date: Fri, 6 Mar 2026 11:05:01 +0000 Subject: [PATCH] Refactor. Add data layer --- .../ApplicationDbContext.cs | 9 +- BudgetApp.Data/BudgetApp.Data.csproj | 22 + .../Entities}/Account.cs | 2 +- .../Entities}/ApplicationUser.cs | 2 +- .../Entities}/Category.cs | 2 +- BudgetApp.Data/Entities/RecurrenceType.cs | 10 + .../Entities}/Salary.cs | 2 +- .../Entities}/Transaction.cs | 17 +- BudgetApp.Data/Entities/TransactionType.cs | 7 + .../Interfaces/IAccountRepository.cs | 14 + .../Interfaces/ICategoryRepository.cs | 12 + .../Interfaces/ISalaryRepository.cs | 12 + .../Interfaces/ITransactionRepository.cs | 20 + ...AddCurrencyCodeAndCategoryIcon.Designer.cs | 4 +- .../AddCurrencyCodeAndCategoryIcon.cs | 41 ++ .../AddIsDefaultToCategory.Designer.cs | 4 +- .../Migrations/AddIsDefaultToCategory.cs | 28 ++ .../AddIsPaidToTransaction.Designer.cs | 4 +- .../Migrations/AddIsPaidToTransaction.cs | 28 ++ .../AddRecurringTransactions.Designer.cs | 4 +- .../Migrations/AddRecurringTransactions.cs | 69 +++ .../Migrations/AddSalary.Designer.cs | 4 +- BudgetApp.Data/Migrations/AddSalary.cs | 102 ++++ .../ApplicationDbContextModelSnapshot.cs | 4 +- .../Migrations/InitialCreate.Designer.cs | 4 +- BudgetApp.Data/Migrations/InitialCreate.cs | 321 ++++++++++++ .../Repositories/AccountRepository.cs | 35 +- .../Repositories/CategoryRepository.cs | 33 +- .../Repositories/SalaryRepository.cs | 37 +- .../Repositories/TransactionRepository.cs | 84 ++-- BudgetApp.Tests/BudgetApp.Tests.csproj | 1 + .../Helpers/TestDbContextFactory.cs | 1 + BudgetApp.Tests/Models/AccountModelTests.cs | 2 +- .../Services/AccountServiceTests.cs | 26 +- .../Services/CategoryServiceTests.cs | 32 +- .../Services/DashboardServiceTests.cs | 22 +- .../Services/TransactionServiceTests.cs | 59 +-- BudgetApp.slnx | 1 + BudgetApp/BudgetApp.csproj | 10 +- BudgetApp/Components/Layout/NavMenu.razor | 48 +- BudgetApp/Components/Layout/NavMenu.razor.cs | 43 ++ BudgetApp/Components/Pages/About.razor | 36 +- BudgetApp/Components/Pages/About.razor.cs | 38 ++ BudgetApp/Components/Pages/Accounts.razor | 162 +----- BudgetApp/Components/Pages/Accounts.razor.cs | 136 +++++ BudgetApp/Components/Pages/Categories.razor | 133 +---- .../Components/Pages/Categories.razor.cs | 118 +++++ BudgetApp/Components/Pages/Home.razor | 471 +++--------------- BudgetApp/Components/Pages/Home.razor.cs | 371 ++++++++++++++ BudgetApp/Components/Pages/Login.razor | 80 +-- BudgetApp/Components/Pages/Login.razor.cs | 74 +++ BudgetApp/Components/Pages/Logout.razor | 1 + BudgetApp/Components/Pages/Register.razor | 47 +- BudgetApp/Components/Pages/Register.razor.cs | 44 ++ BudgetApp/Components/Pages/Reports.razor | 262 ++-------- BudgetApp/Components/Pages/Reports.razor.cs | 218 ++++++++ BudgetApp/Components/Pages/Salary.razor | 140 +----- BudgetApp/Components/Pages/Salary.razor.cs | 113 +++++ BudgetApp/Components/Pages/Settings.razor | 198 +------- BudgetApp/Components/Pages/Settings.razor.cs | 159 ++++++ BudgetApp/Components/Pages/Transactions.razor | 466 +++-------------- .../Components/Pages/Transactions.razor.cs | 352 +++++++++++++ BudgetApp/Components/Pages/Weather.razor | 64 --- BudgetApp/Components/ToastContainer.razor | 42 +- BudgetApp/Components/ToastContainer.razor.cs | 40 ++ BudgetApp/Components/ToastValidator.razor | 46 +- BudgetApp/Components/ToastValidator.razor.cs | 48 ++ BudgetApp/Components/_Imports.razor | 2 + .../20260301152724_InitialCreate.cs | 323 ------------ ...20260301155815_AddRecurringTransactions.cs | 71 --- ...01160737_AddCurrencyCodeAndCategoryIcon.cs | 42 -- .../20260305164332_AddIsPaidToTransaction.cs | 29 -- .../Migrations/20260305175841_AddSalary.cs | 104 ---- .../20260305181148_AddIsDefaultToCategory.cs | 29 -- BudgetApp/Program.cs | 17 +- BudgetApp/Services/DashboardService.cs | 52 +- BudgetApp/Services/Models/ChartData.cs | 8 + BudgetApp/Services/Models/ChartDataset.cs | 9 + BudgetApp/Services/Models/DashboardData.cs | 14 + BudgetApp/budgetapp.db | Bin 167936 -> 167936 bytes BudgetApp/budgetapp.db-shm | Bin 32768 -> 32768 bytes BudgetApp/budgetapp.db-wal | Bin 449112 -> 41232 bytes 82 files changed, 2930 insertions(+), 2811 deletions(-) rename {BudgetApp/Data => BudgetApp.Data}/ApplicationDbContext.cs (92%) create mode 100644 BudgetApp.Data/BudgetApp.Data.csproj rename {BudgetApp/Data => BudgetApp.Data/Entities}/Account.cs (97%) rename {BudgetApp/Data => BudgetApp.Data/Entities}/ApplicationUser.cs (71%) rename {BudgetApp/Data => BudgetApp.Data/Entities}/Category.cs (93%) create mode 100644 BudgetApp.Data/Entities/RecurrenceType.cs rename {BudgetApp/Data => BudgetApp.Data/Entities}/Salary.cs (97%) rename {BudgetApp/Data => BudgetApp.Data/Entities}/Transaction.cs (84%) create mode 100644 BudgetApp.Data/Entities/TransactionType.cs create mode 100644 BudgetApp.Data/Interfaces/IAccountRepository.cs create mode 100644 BudgetApp.Data/Interfaces/ICategoryRepository.cs create mode 100644 BudgetApp.Data/Interfaces/ISalaryRepository.cs create mode 100644 BudgetApp.Data/Interfaces/ITransactionRepository.cs rename BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs => BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs rename BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs => BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs rename BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs => BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs rename BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs => BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/AddRecurringTransactions.cs rename BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs => BudgetApp.Data/Migrations/AddSalary.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/AddSalary.cs rename {BudgetApp => BudgetApp.Data}/Migrations/ApplicationDbContextModelSnapshot.cs (99%) rename BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs => BudgetApp.Data/Migrations/InitialCreate.Designer.cs (99%) create mode 100644 BudgetApp.Data/Migrations/InitialCreate.cs rename BudgetApp/Services/AccountService.cs => BudgetApp.Data/Repositories/AccountRepository.cs (67%) rename BudgetApp/Services/CategoryService.cs => BudgetApp.Data/Repositories/CategoryRepository.cs (79%) rename BudgetApp/Services/SalaryService.cs => BudgetApp.Data/Repositories/SalaryRepository.cs (79%) rename BudgetApp/Services/TransactionService.cs => BudgetApp.Data/Repositories/TransactionRepository.cs (78%) create mode 100644 BudgetApp/Components/Layout/NavMenu.razor.cs create mode 100644 BudgetApp/Components/Pages/About.razor.cs create mode 100644 BudgetApp/Components/Pages/Categories.razor.cs create mode 100644 BudgetApp/Components/Pages/Home.razor.cs create mode 100644 BudgetApp/Components/Pages/Register.razor.cs create mode 100644 BudgetApp/Components/Pages/Reports.razor.cs create mode 100644 BudgetApp/Components/Pages/Salary.razor.cs create mode 100644 BudgetApp/Components/Pages/Settings.razor.cs delete mode 100644 BudgetApp/Components/Pages/Weather.razor create mode 100644 BudgetApp/Components/ToastContainer.razor.cs create mode 100644 BudgetApp/Components/ToastValidator.razor.cs delete mode 100644 BudgetApp/Migrations/20260301152724_InitialCreate.cs delete mode 100644 BudgetApp/Migrations/20260301155815_AddRecurringTransactions.cs delete mode 100644 BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.cs delete mode 100644 BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs delete mode 100644 BudgetApp/Migrations/20260305175841_AddSalary.cs delete mode 100644 BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs create mode 100644 BudgetApp/Services/Models/ChartData.cs create mode 100644 BudgetApp/Services/Models/ChartDataset.cs create mode 100644 BudgetApp/Services/Models/DashboardData.cs diff --git a/BudgetApp/Data/ApplicationDbContext.cs b/BudgetApp.Data/ApplicationDbContext.cs similarity index 92% rename from BudgetApp/Data/ApplicationDbContext.cs rename to BudgetApp.Data/ApplicationDbContext.cs index 9edeb55..883a974 100644 --- a/BudgetApp/Data/ApplicationDbContext.cs +++ b/BudgetApp.Data/ApplicationDbContext.cs @@ -1,15 +1,12 @@ +using BudgetApp.Data.Entities; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace BudgetApp.Data; -public class ApplicationDbContext : IdentityDbContext +public class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options) { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - public DbSet Accounts => Set(); public DbSet Transactions => Set(); public DbSet Categories => Set(); diff --git a/BudgetApp.Data/BudgetApp.Data.csproj b/BudgetApp.Data/BudgetApp.Data.csproj new file mode 100644 index 0000000..becae22 --- /dev/null +++ b/BudgetApp.Data/BudgetApp.Data.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/BudgetApp/Data/Account.cs b/BudgetApp.Data/Entities/Account.cs similarity index 97% rename from BudgetApp/Data/Account.cs rename to BudgetApp.Data/Entities/Account.cs index db0354a..44ac8a6 100644 --- a/BudgetApp/Data/Account.cs +++ b/BudgetApp.Data/Entities/Account.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace BudgetApp.Data; +namespace BudgetApp.Data.Entities; public class Account { diff --git a/BudgetApp/Data/ApplicationUser.cs b/BudgetApp.Data/Entities/ApplicationUser.cs similarity index 71% rename from BudgetApp/Data/ApplicationUser.cs rename to BudgetApp.Data/Entities/ApplicationUser.cs index 3e0c153..a64a86f 100644 --- a/BudgetApp/Data/ApplicationUser.cs +++ b/BudgetApp.Data/Entities/ApplicationUser.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace BudgetApp.Data; +namespace BudgetApp.Data.Entities; public class ApplicationUser : IdentityUser { diff --git a/BudgetApp/Data/Category.cs b/BudgetApp.Data/Entities/Category.cs similarity index 93% rename from BudgetApp/Data/Category.cs rename to BudgetApp.Data/Entities/Category.cs index 85037e6..935bc1d 100644 --- a/BudgetApp/Data/Category.cs +++ b/BudgetApp.Data/Entities/Category.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace BudgetApp.Data; +namespace BudgetApp.Data.Entities; public class Category { diff --git a/BudgetApp.Data/Entities/RecurrenceType.cs b/BudgetApp.Data/Entities/RecurrenceType.cs new file mode 100644 index 0000000..45a7990 --- /dev/null +++ b/BudgetApp.Data/Entities/RecurrenceType.cs @@ -0,0 +1,10 @@ +namespace BudgetApp.Data.Entities; + +public enum RecurrenceType +{ + None, + Daily, + Weekly, + Monthly, + Yearly +} \ No newline at end of file diff --git a/BudgetApp/Data/Salary.cs b/BudgetApp.Data/Entities/Salary.cs similarity index 97% rename from BudgetApp/Data/Salary.cs rename to BudgetApp.Data/Entities/Salary.cs index c26fc6a..bb8fca9 100644 --- a/BudgetApp/Data/Salary.cs +++ b/BudgetApp.Data/Entities/Salary.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace BudgetApp.Data; +namespace BudgetApp.Data.Entities; public class Salary { diff --git a/BudgetApp/Data/Transaction.cs b/BudgetApp.Data/Entities/Transaction.cs similarity index 84% rename from BudgetApp/Data/Transaction.cs rename to BudgetApp.Data/Entities/Transaction.cs index 8cfaf05..b4ff280 100644 --- a/BudgetApp/Data/Transaction.cs +++ b/BudgetApp.Data/Entities/Transaction.cs @@ -1,21 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace BudgetApp.Data; - -public enum TransactionType -{ - Income, - Expense -} - -public enum RecurrenceType -{ - None, - Daily, - Weekly, - Monthly, - Yearly -} +namespace BudgetApp.Data.Entities; public class Transaction { diff --git a/BudgetApp.Data/Entities/TransactionType.cs b/BudgetApp.Data/Entities/TransactionType.cs new file mode 100644 index 0000000..147f97c --- /dev/null +++ b/BudgetApp.Data/Entities/TransactionType.cs @@ -0,0 +1,7 @@ +namespace BudgetApp.Data.Entities; + +public enum TransactionType +{ + Income, + Expense +} \ No newline at end of file diff --git a/BudgetApp.Data/Interfaces/IAccountRepository.cs b/BudgetApp.Data/Interfaces/IAccountRepository.cs new file mode 100644 index 0000000..79fbad2 --- /dev/null +++ b/BudgetApp.Data/Interfaces/IAccountRepository.cs @@ -0,0 +1,14 @@ +using BudgetApp.Data.Entities; + +namespace BudgetApp.Data.Interfaces; + +public interface IAccountRepository +{ + Task> GetAccountsAsync(string userId); + Task GetAccountAsync(int id, string userId); + Task CreateAccountAsync(Account account); + Task UpdateAccountAsync(Account account); + Task UpdateBalanceAsync(int id, string userId, decimal newBalance); + Task UpdateAccountDetailsAsync(int id, string userId, string name, string color, string currencyCode); + Task DeleteAccountAsync(int id, string userId); +} diff --git a/BudgetApp.Data/Interfaces/ICategoryRepository.cs b/BudgetApp.Data/Interfaces/ICategoryRepository.cs new file mode 100644 index 0000000..8a1fba0 --- /dev/null +++ b/BudgetApp.Data/Interfaces/ICategoryRepository.cs @@ -0,0 +1,12 @@ +using BudgetApp.Data.Entities; + +namespace BudgetApp.Data.Interfaces; + +public interface ICategoryRepository +{ + Task> GetCategoriesAsync(string userId); + Task EnsureDefaultCategoriesAsync(string userId); + Task CreateCategoryAsync(Category category); + Task UpdateCategoryAsync(int id, string userId, string name, string icon); + Task DeleteCategoryAsync(int id, string userId); +} diff --git a/BudgetApp.Data/Interfaces/ISalaryRepository.cs b/BudgetApp.Data/Interfaces/ISalaryRepository.cs new file mode 100644 index 0000000..0fe2096 --- /dev/null +++ b/BudgetApp.Data/Interfaces/ISalaryRepository.cs @@ -0,0 +1,12 @@ +using BudgetApp.Data.Entities; + +namespace BudgetApp.Data.Interfaces; + +public interface ISalaryRepository +{ + Task> GetSalariesAsync(string userId); + Task CreateSalaryAsync(Salary salary, DateTime windowEnd); + Task DeleteSalaryAsync(int id, string userId); + Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd); + Task GetNextPaydateAsync(int salaryId); +} diff --git a/BudgetApp.Data/Interfaces/ITransactionRepository.cs b/BudgetApp.Data/Interfaces/ITransactionRepository.cs new file mode 100644 index 0000000..3c129c2 --- /dev/null +++ b/BudgetApp.Data/Interfaces/ITransactionRepository.cs @@ -0,0 +1,20 @@ +using BudgetApp.Data.Entities; + +namespace BudgetApp.Data.Interfaces; + +public interface ITransactionRepository +{ + Task> GetTransactionsAsync(string userId); + Task> GetRecentTransactionsAsync(string userId, int count = 5); + Task> GetUpcomingTransactionsAsync(string userId, int count = 5); + Task> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to); + Task> GetTransactionsByAccountAsync(int accountId, string userId); + Task CreateTransactionAsync(Transaction transaction); + 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); + Task DeleteTransactionAsync(int id, string userId); + Task DeleteTransactionSeriesAsync(int id, string userId); + Task MarkAsPaidAsync(int id, string userId, bool isPaid = true); + Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null); +} diff --git a/BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs rename to BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs index 9cab8eb..e03aa83 100644 --- a/BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs +++ b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260301160737_AddCurrencyCodeAndCategoryIcon")] diff --git a/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs new file mode 100644 index 0000000..ddb6c24 --- /dev/null +++ b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class AddCurrencyCodeAndCategoryIcon : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Icon", + table: "Categories", + type: "TEXT", + maxLength: 10, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "CurrencyCode", + table: "Accounts", + type: "TEXT", + maxLength: 3, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Icon", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "CurrencyCode", + table: "Accounts"); + } +} \ No newline at end of file diff --git a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs rename to BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs index a622392..838e735 100644 --- a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs +++ b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260305181148_AddIsDefaultToCategory")] diff --git a/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs new file mode 100644 index 0000000..e893ea9 --- /dev/null +++ b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class AddIsDefaultToCategory : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDefault", + table: "Categories", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDefault", + table: "Categories"); + } +} \ No newline at end of file diff --git a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs rename to BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs index 877119d..cf0442a 100644 --- a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs +++ b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260305164332_AddIsPaidToTransaction")] diff --git a/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs new file mode 100644 index 0000000..63eb825 --- /dev/null +++ b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class AddIsPaidToTransaction : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPaid", + table: "Transactions", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPaid", + table: "Transactions"); + } +} \ No newline at end of file diff --git a/BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs b/BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs rename to BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs index 5e7016e..e0d2210 100644 --- a/BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs +++ b/BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260301155815_AddRecurringTransactions")] diff --git a/BudgetApp.Data/Migrations/AddRecurringTransactions.cs b/BudgetApp.Data/Migrations/AddRecurringTransactions.cs new file mode 100644 index 0000000..3d11cda --- /dev/null +++ b/BudgetApp.Data/Migrations/AddRecurringTransactions.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class AddRecurringTransactions : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ParentTransactionId", + table: "Transactions", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "RecurrenceEndDate", + table: "Transactions", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "RecurrenceType", + table: "Transactions", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_ParentTransactionId", + table: "Transactions", + column: "ParentTransactionId"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Transactions_ParentTransactionId", + table: "Transactions", + column: "ParentTransactionId", + principalTable: "Transactions", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Transactions_ParentTransactionId", + table: "Transactions"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_ParentTransactionId", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "ParentTransactionId", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "RecurrenceEndDate", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "RecurrenceType", + table: "Transactions"); + } +} \ No newline at end of file diff --git a/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs b/BudgetApp.Data/Migrations/AddSalary.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs rename to BudgetApp.Data/Migrations/AddSalary.Designer.cs index 951ae4a..d868977 100644 --- a/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs +++ b/BudgetApp.Data/Migrations/AddSalary.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260305175841_AddSalary")] diff --git a/BudgetApp.Data/Migrations/AddSalary.cs b/BudgetApp.Data/Migrations/AddSalary.cs new file mode 100644 index 0000000..3bfcb30 --- /dev/null +++ b/BudgetApp.Data/Migrations/AddSalary.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class AddSalary : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SalaryId", + table: "Transactions", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "Salaries", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Amount = table.Column(type: "decimal(18,2)", nullable: false), + RecurrenceType = table.Column(type: "INTEGER", nullable: false), + StartDate = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "INTEGER", nullable: false), + CategoryId = table.Column(type: "INTEGER", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Salaries", x => x.Id); + table.ForeignKey( + name: "FK_Salaries_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Salaries_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Salaries_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_SalaryId", + table: "Transactions", + column: "SalaryId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_AccountId", + table: "Salaries", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_CategoryId", + table: "Salaries", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_UserId", + table: "Salaries", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Salaries_SalaryId", + table: "Transactions", + column: "SalaryId", + principalTable: "Salaries", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Salaries_SalaryId", + table: "Transactions"); + + migrationBuilder.DropTable( + name: "Salaries"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_SalaryId", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "SalaryId", + table: "Transactions"); + } +} \ No newline at end of file diff --git a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs b/BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs similarity index 99% rename from BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs rename to BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 92d0001..0163219 100644 --- a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot diff --git a/BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs b/BudgetApp.Data/Migrations/InitialCreate.Designer.cs similarity index 99% rename from BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs rename to BudgetApp.Data/Migrations/InitialCreate.Designer.cs index 833968f..de2bdc1 100644 --- a/BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs +++ b/BudgetApp.Data/Migrations/InitialCreate.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using BudgetApp.Data; using Microsoft.EntityFrameworkCore; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace BudgetApp.Migrations +namespace BudgetApp.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20260301152724_InitialCreate")] diff --git a/BudgetApp.Data/Migrations/InitialCreate.cs b/BudgetApp.Data/Migrations/InitialCreate.cs new file mode 100644 index 0000000..ee00287 --- /dev/null +++ b/BudgetApp.Data/Migrations/InitialCreate.cs @@ -0,0 +1,321 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Data.Migrations; + +/// +public partial class InitialCreate : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Accounts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CurrentBalance = table.Column(type: "decimal(18,2)", nullable: false), + Color = table.Column(type: "TEXT", maxLength: 7, nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Accounts", x => x.Id); + table.ForeignKey( + name: "FK_Accounts_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + table.ForeignKey( + name: "FK_Categories_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Transactions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Description = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Amount = table.Column(type: "decimal(18,2)", nullable: false), + Date = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + AccountId = table.Column(type: "INTEGER", nullable: false), + CategoryId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Transactions", x => x.Id); + table.ForeignKey( + name: "FK_Transactions_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Transactions_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_UserId", + table: "Accounts", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Categories_UserId", + table: "Categories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_AccountId", + table: "Transactions", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_CategoryId", + table: "Transactions", + column: "CategoryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Transactions"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "Accounts"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } +} \ No newline at end of file diff --git a/BudgetApp/Services/AccountService.cs b/BudgetApp.Data/Repositories/AccountRepository.cs similarity index 67% rename from BudgetApp/Services/AccountService.cs rename to BudgetApp.Data/Repositories/AccountRepository.cs index 45dd303..4be0fb9 100644 --- a/BudgetApp/Services/AccountService.cs +++ b/BudgetApp.Data/Repositories/AccountRepository.cs @@ -1,15 +1,14 @@ +using BudgetApp.Data.Entities; +using BudgetApp.Data.Interfaces; using Microsoft.EntityFrameworkCore; -using BudgetApp.Data; -namespace BudgetApp.Services; +namespace BudgetApp.Data.Repositories; -public class AccountService(ApplicationDbContext db) +public class AccountRepository(ApplicationDbContext db) : IAccountRepository { - private readonly ApplicationDbContext _db = db; - public async Task> GetAccountsAsync(string userId) { - return await _db.Accounts + return await db.Accounts .Where(a => a.UserId == userId) .OrderBy(a => a.Name) .ToListAsync(); @@ -17,54 +16,54 @@ public class AccountService(ApplicationDbContext db) public async Task GetAccountAsync(int id, string userId) { - return await _db.Accounts + return await db.Accounts .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId); } public async Task CreateAccountAsync(Account account) { - _db.Accounts.Add(account); - await _db.SaveChangesAsync(); + db.Accounts.Add(account); + await db.SaveChangesAsync(); } public async Task UpdateAccountAsync(Account account) { - _db.Accounts.Update(account); - await _db.SaveChangesAsync(); + db.Accounts.Update(account); + await db.SaveChangesAsync(); } public async Task UpdateBalanceAsync(int id, string userId, decimal newBalance) { - var account = await _db.Accounts + var account = await db.Accounts .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId); if (account is not null) { account.CurrentBalance = newBalance; - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } } public async Task UpdateAccountDetailsAsync(int id, string userId, string name, string color, string currencyCode) { - var account = await _db.Accounts + var account = await db.Accounts .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId); if (account is not null) { account.Name = name; account.Color = color; account.CurrencyCode = currencyCode; - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } } public async Task DeleteAccountAsync(int id, string userId) { - var account = await _db.Accounts + var account = await db.Accounts .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId); if (account is not null) { - _db.Accounts.Remove(account); - await _db.SaveChangesAsync(); + db.Accounts.Remove(account); + await db.SaveChangesAsync(); } } } diff --git a/BudgetApp/Services/CategoryService.cs b/BudgetApp.Data/Repositories/CategoryRepository.cs similarity index 79% rename from BudgetApp/Services/CategoryService.cs rename to BudgetApp.Data/Repositories/CategoryRepository.cs index e7cc5af..9f04ef8 100644 --- a/BudgetApp/Services/CategoryService.cs +++ b/BudgetApp.Data/Repositories/CategoryRepository.cs @@ -1,12 +1,11 @@ +using BudgetApp.Data.Entities; +using BudgetApp.Data.Interfaces; using Microsoft.EntityFrameworkCore; -using BudgetApp.Data; -namespace BudgetApp.Services; +namespace BudgetApp.Data.Repositories; -public class CategoryService(ApplicationDbContext db) +public class CategoryRepository(ApplicationDbContext db) : ICategoryRepository { - private readonly ApplicationDbContext _db = db; - private static readonly (string Name, string Icon)[] DefaultCategories = [ ("Groceries", "🛒"), @@ -31,7 +30,7 @@ public class CategoryService(ApplicationDbContext db) public async Task> GetCategoriesAsync(string userId) { - return await _db.Categories + return await db.Categories .Where(c => c.UserId == userId) .OrderBy(c => c.Name) .ToListAsync(); @@ -39,7 +38,7 @@ public class CategoryService(ApplicationDbContext db) public async Task EnsureDefaultCategoriesAsync(string userId) { - var existing = await _db.Categories + var existing = await db.Categories .Where(c => c.UserId == userId) .ToListAsync(); @@ -53,8 +52,8 @@ public class CategoryService(ApplicationDbContext db) UserId = userId }); - _db.Categories.AddRange(defaults); - await _db.SaveChangesAsync(); + db.Categories.AddRange(defaults); + await db.SaveChangesAsync(); return; } @@ -78,38 +77,38 @@ public class CategoryService(ApplicationDbContext db) if (updated) { - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } } public async Task CreateCategoryAsync(Category category) { - _db.Categories.Add(category); - await _db.SaveChangesAsync(); + db.Categories.Add(category); + await db.SaveChangesAsync(); } public async Task UpdateCategoryAsync(int id, string userId, string name, string icon) { - var category = await _db.Categories + var category = await db.Categories .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId); if (category is not null) { if (category.IsDefault) return; category.Name = name; category.Icon = icon; - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } } public async Task DeleteCategoryAsync(int id, string userId) { - var category = await _db.Categories + var category = await db.Categories .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId); if (category is not null) { if (category.IsDefault) return; - _db.Categories.Remove(category); - await _db.SaveChangesAsync(); + db.Categories.Remove(category); + await db.SaveChangesAsync(); } } } diff --git a/BudgetApp/Services/SalaryService.cs b/BudgetApp.Data/Repositories/SalaryRepository.cs similarity index 79% rename from BudgetApp/Services/SalaryService.cs rename to BudgetApp.Data/Repositories/SalaryRepository.cs index 1e1bc37..f8d6825 100644 --- a/BudgetApp/Services/SalaryService.cs +++ b/BudgetApp.Data/Repositories/SalaryRepository.cs @@ -1,15 +1,14 @@ +using BudgetApp.Data.Entities; +using BudgetApp.Data.Interfaces; using Microsoft.EntityFrameworkCore; -using BudgetApp.Data; -namespace BudgetApp.Services; +namespace BudgetApp.Data.Repositories; -public class SalaryService(ApplicationDbContext db) +public class SalaryRepository(ApplicationDbContext db) : ISalaryRepository { - private readonly ApplicationDbContext _db = db; - public async Task> GetSalariesAsync(string userId) { - return await _db.Salaries + return await db.Salaries .Include(s => s.Account) .Include(s => s.Category) .Where(s => s.UserId == userId) @@ -19,35 +18,35 @@ public class SalaryService(ApplicationDbContext db) public async Task CreateSalaryAsync(Salary salary, DateTime windowEnd) { - var salaryCategoryId = await _db.Categories + var salaryCategoryId = await db.Categories .Where(c => c.UserId == salary.UserId && c.Name == "Salary") .Select(c => (int?)c.Id) .FirstOrDefaultAsync(); salary.CategoryId = salaryCategoryId; - _db.Salaries.Add(salary); - await _db.SaveChangesAsync(); + db.Salaries.Add(salary); + await db.SaveChangesAsync(); await MaterializeSalaryTransactionsAsync(salary.UserId, windowEnd); } public async Task DeleteSalaryAsync(int id, string userId) { - var exists = await _db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId); + var exists = await db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId); if (!exists) return; - await using var transaction = await _db.Database.BeginTransactionAsync(); + await using var transaction = await db.Database.BeginTransactionAsync(); // Delete future unpaid transactions generated by this salary - await _db.Transactions + await db.Transactions .Where(t => t.SalaryId == id && t.Date.Date > DateTime.Today && !t.IsPaid) .ExecuteDeleteAsync(); // Unlink past / paid transactions so they stay in history without the salary reference - await _db.Transactions + await db.Transactions .Where(t => t.SalaryId == id) .ExecuteUpdateAsync(s => s.SetProperty(t => t.SalaryId, (int?)null)); - await _db.Salaries + await db.Salaries .Where(s => s.Id == id && s.UserId == userId) .ExecuteDeleteAsync(); @@ -56,7 +55,7 @@ public class SalaryService(ApplicationDbContext db) public async Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd) { - var salaries = await _db.Salaries + var salaries = await db.Salaries .Where(s => s.UserId == userId) .ToListAsync(); @@ -64,7 +63,7 @@ public class SalaryService(ApplicationDbContext db) foreach (var salary in salaries) { - var latestDate = await _db.Transactions + var latestDate = await db.Transactions .Where(t => t.SalaryId == salary.Id) .OrderByDescending(t => t.Date) .Select(t => (DateTime?)t.Date) @@ -93,14 +92,14 @@ public class SalaryService(ApplicationDbContext db) if (newTransactions.Count > 0) { - _db.Transactions.AddRange(newTransactions); - await _db.SaveChangesAsync(); + db.Transactions.AddRange(newTransactions); + await db.SaveChangesAsync(); } } public async Task GetNextPaydateAsync(int salaryId) { - return await _db.Transactions + return await db.Transactions .Where(t => t.SalaryId == salaryId && t.Date.Date >= DateTime.Today) .OrderBy(t => t.Date) .Select(t => (DateTime?)t.Date) diff --git a/BudgetApp/Services/TransactionService.cs b/BudgetApp.Data/Repositories/TransactionRepository.cs similarity index 78% rename from BudgetApp/Services/TransactionService.cs rename to BudgetApp.Data/Repositories/TransactionRepository.cs index 6bb0eb0..01677dc 100644 --- a/BudgetApp/Services/TransactionService.cs +++ b/BudgetApp.Data/Repositories/TransactionRepository.cs @@ -1,15 +1,14 @@ +using BudgetApp.Data.Entities; +using BudgetApp.Data.Interfaces; using Microsoft.EntityFrameworkCore; -using BudgetApp.Data; -namespace BudgetApp.Services; +namespace BudgetApp.Data.Repositories; -public class TransactionService(ApplicationDbContext db) +public class TransactionRepository(ApplicationDbContext db) : ITransactionRepository { - private readonly ApplicationDbContext _db = db; - public async Task> GetTransactionsAsync(string userId) { - return await _db.Transactions + return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) @@ -20,7 +19,7 @@ public class TransactionService(ApplicationDbContext db) public async Task> GetRecentTransactionsAsync(string userId, int count = 5) { - return await _db.Transactions + return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) @@ -32,7 +31,7 @@ public class TransactionService(ApplicationDbContext db) public async Task> GetUpcomingTransactionsAsync(string userId, int count = 5) { - return await _db.Transactions + return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) @@ -44,7 +43,7 @@ public class TransactionService(ApplicationDbContext db) public async Task> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to) { - return await _db.Transactions + return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) @@ -55,7 +54,7 @@ public class TransactionService(ApplicationDbContext db) public async Task> GetTransactionsByAccountAsync(int accountId, string userId) { - return await _db.Transactions + return await db.Transactions .Include(t => t.Category) .Where(t => t.AccountId == accountId && t.Account.UserId == userId) .OrderByDescending(t => t.Date) @@ -74,8 +73,8 @@ public class TransactionService(ApplicationDbContext db) } } - _db.Transactions.Add(transaction); - await _db.SaveChangesAsync(); + db.Transactions.Add(transaction); + await db.SaveChangesAsync(); // Immediately materialize child occurrences for recurring transactions if (transaction.RecurrenceType != RecurrenceType.None) @@ -102,15 +101,17 @@ public class TransactionService(ApplicationDbContext db) if (newTransactions.Count > 0) { - _db.Transactions.AddRange(newTransactions); - await _db.SaveChangesAsync(); + 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) + 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 + var transaction = await db.Transactions .Include(t => t.Account) .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); @@ -128,11 +129,10 @@ public class TransactionService(ApplicationDbContext db) if (isPartOfSeries) { - // Cascade edits to all future occurrences in the series var parentId = transaction.ParentTransactionId ?? transaction.Id; var cutoffDate = transaction.Date.Date; - var futureSiblings = await _db.Transactions + var futureSiblings = await db.Transactions .Where(t => (t.Id == parentId || t.ParentTransactionId == parentId) && t.Id != id && t.Date.Date >= cutoffDate) @@ -149,92 +149,84 @@ public class TransactionService(ApplicationDbContext db) } else { - // Standalone transaction — preserve existing recurrence state (None) transaction.RecurrenceType = recurrenceType; transaction.RecurrenceEndDate = recurrenceEndDate; } - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } public async Task DeleteTransactionAsync(int id, string userId) { - var transaction = await _db.Transactions + 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(); + db.Transactions.Remove(transaction); + await db.SaveChangesAsync(); } } public async Task DeleteTransactionSeriesAsync(int id, string userId) { - var transaction = await _db.Transactions + var transaction = await db.Transactions .Include(t => t.Account) .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); if (transaction is null) return; - // Resolve the parent id: if this is a child, use its ParentTransactionId; otherwise use its own Id var parentId = transaction.ParentTransactionId ?? transaction.Id; - - // Use the clicked transaction's date as the cutoff — delete this one and everything after it var cutoffDate = transaction.Date.Date; - var allChildren = await _db.Transactions + 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); + db.Transactions.RemoveRange(toDelete); - // Handle the parent var parent = parentId == transaction.Id ? transaction - : await _db.Transactions + : await db.Transactions .FirstOrDefaultAsync(t => t.Id == parentId && t.Account.UserId == userId); if (parent is not null) { if (parent.Date.Date >= cutoffDate) { - // Parent is also at/after the cutoff — detach earlier children so - // the DB-level ON DELETE SET NULL doesn't clear their link, then delete foreach (var child in toKeep) { child.ParentTransactionId = null; } - _db.Transactions.Remove(parent); + db.Transactions.Remove(parent); } else { - // Parent is before the cutoff — cap recurrence so materialization stops parent.RecurrenceEndDate = cutoffDate.AddDays(-1); } } - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true) { - var transaction = await _db.Transactions + 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(); + await db.SaveChangesAsync(); } public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null) { - var recurringParents = await _db.Transactions + var recurringParents = await db.Transactions .Include(t => t.Account) .Where(t => t.Account.UserId == userId && t.RecurrenceType != RecurrenceType.None @@ -251,7 +243,7 @@ public class TransactionService(ApplicationDbContext db) ? parent.RecurrenceEndDate.Value : horizon; - var latestChildDate = await _db.Transactions + var latestChildDate = await db.Transactions .Where(t => t.ParentTransactionId == parent.Id) .OrderByDescending(t => t.Date) .Select(t => (DateTime?)t.Date) @@ -279,17 +271,17 @@ public class TransactionService(ApplicationDbContext db) if (newTransactions.Count > 0) { - _db.Transactions.AddRange(newTransactions); - await _db.SaveChangesAsync(); + 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.Daily => current.AddDays(1), + RecurrenceType.Weekly => current.AddDays(7), RecurrenceType.Monthly => current.AddMonths(1), - RecurrenceType.Yearly => current.AddYears(1), - _ => DateTime.MaxValue + RecurrenceType.Yearly => current.AddYears(1), + _ => DateTime.MaxValue }; } diff --git a/BudgetApp.Tests/BudgetApp.Tests.csproj b/BudgetApp.Tests/BudgetApp.Tests.csproj index 3ae0926..53ac245 100644 --- a/BudgetApp.Tests/BudgetApp.Tests.csproj +++ b/BudgetApp.Tests/BudgetApp.Tests.csproj @@ -23,6 +23,7 @@ + diff --git a/BudgetApp.Tests/Helpers/TestDbContextFactory.cs b/BudgetApp.Tests/Helpers/TestDbContextFactory.cs index 6418c0c..3377907 100644 --- a/BudgetApp.Tests/Helpers/TestDbContextFactory.cs +++ b/BudgetApp.Tests/Helpers/TestDbContextFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using BudgetApp.Data; +using BudgetApp.Data.Entities; namespace BudgetApp.Tests.Helpers; diff --git a/BudgetApp.Tests/Models/AccountModelTests.cs b/BudgetApp.Tests/Models/AccountModelTests.cs index e256c0c..d743bc0 100644 --- a/BudgetApp.Tests/Models/AccountModelTests.cs +++ b/BudgetApp.Tests/Models/AccountModelTests.cs @@ -1,4 +1,4 @@ -using BudgetApp.Data; +using BudgetApp.Data.Entities; using Xunit; namespace BudgetApp.Tests.Models; diff --git a/BudgetApp.Tests/Services/AccountServiceTests.cs b/BudgetApp.Tests/Services/AccountServiceTests.cs index 2556b01..ed29e0c 100644 --- a/BudgetApp.Tests/Services/AccountServiceTests.cs +++ b/BudgetApp.Tests/Services/AccountServiceTests.cs @@ -1,5 +1,5 @@ -using BudgetApp.Data; -using BudgetApp.Services; +using BudgetApp.Data.Entities; +using BudgetApp.Data.Repositories; using BudgetApp.Tests.Helpers; using Xunit; @@ -20,7 +20,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); var result = await service.GetAccountsAsync(TestDbContextFactory.TestUserId); @@ -41,7 +41,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); var result = await service.GetAccountsAsync(TestDbContextFactory.TestUserId); @@ -63,7 +63,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); var result = await service.GetAccountAsync(accountId, TestDbContextFactory.TestUserId); @@ -85,7 +85,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); var result = await service.GetAccountAsync(accountId, TestDbContextFactory.OtherUserId); @@ -97,7 +97,7 @@ public class AccountServiceTests { using var factory = new TestDbContextFactory(); using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); var account = new Account { Name = "New", UserId = TestDbContextFactory.TestUserId, CurrencyCode = "GBP" }; await service.CreateAccountAsync(account); @@ -120,7 +120,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.UpdateBalanceAsync(accountId, TestDbContextFactory.TestUserId, 250m); using var verifyCtx = factory.CreateContext(); @@ -142,7 +142,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.UpdateBalanceAsync(accountId, TestDbContextFactory.OtherUserId, 999m); using var verifyCtx = factory.CreateContext(); @@ -163,7 +163,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.UpdateAccountDetailsAsync(accountId, TestDbContextFactory.TestUserId, "New", "#fff", "USD"); using var verifyCtx = factory.CreateContext(); @@ -187,7 +187,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.UpdateAccountDetailsAsync(accountId, TestDbContextFactory.OtherUserId, "Hacked", "#fff", "USD"); using var verifyCtx = factory.CreateContext(); @@ -208,7 +208,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.DeleteAccountAsync(accountId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -229,7 +229,7 @@ public class AccountServiceTests } using var context = factory.CreateContext(); - var service = new AccountService(context); + var service = new AccountRepository(context); await service.DeleteAccountAsync(accountId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); diff --git a/BudgetApp.Tests/Services/CategoryServiceTests.cs b/BudgetApp.Tests/Services/CategoryServiceTests.cs index 1536f63..c417cf4 100644 --- a/BudgetApp.Tests/Services/CategoryServiceTests.cs +++ b/BudgetApp.Tests/Services/CategoryServiceTests.cs @@ -1,5 +1,5 @@ -using BudgetApp.Data; -using BudgetApp.Services; +using BudgetApp.Data.Entities; +using BudgetApp.Data.Repositories; using BudgetApp.Tests.Helpers; using Microsoft.EntityFrameworkCore; using Xunit; @@ -21,7 +21,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId); @@ -42,7 +42,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId); @@ -55,7 +55,7 @@ public class CategoryServiceTests { using var factory = new TestDbContextFactory(); using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); @@ -77,13 +77,13 @@ public class CategoryServiceTests using (var ctx = factory.CreateContext()) { - var service = new CategoryService(ctx); + var service = new CategoryRepository(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } using (var ctx = factory.CreateContext()) { - var service = new CategoryService(ctx); + var service = new CategoryRepository(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } @@ -111,7 +111,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -137,7 +137,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -154,13 +154,13 @@ public class CategoryServiceTests using (var ctx = factory.CreateContext()) { - var service = new CategoryService(ctx); + var service = new CategoryRepository(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } using (var ctx = factory.CreateContext()) { - var service = new CategoryService(ctx); + var service = new CategoryRepository(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.OtherUserId); } @@ -174,7 +174,7 @@ public class CategoryServiceTests { using var factory = new TestDbContextFactory(); using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.CreateCategoryAsync(new Category { @@ -201,7 +201,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.TestUserId, "New", "🎯"); using var verifyCtx = factory.CreateContext(); @@ -224,7 +224,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.OtherUserId, "Hacked", "💀"); using var verifyCtx = factory.CreateContext(); @@ -245,7 +245,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -266,7 +266,7 @@ public class CategoryServiceTests } using var context = factory.CreateContext(); - var service = new CategoryService(context); + var service = new CategoryRepository(context); await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); diff --git a/BudgetApp.Tests/Services/DashboardServiceTests.cs b/BudgetApp.Tests/Services/DashboardServiceTests.cs index 4cadb1e..863cf55 100644 --- a/BudgetApp.Tests/Services/DashboardServiceTests.cs +++ b/BudgetApp.Tests/Services/DashboardServiceTests.cs @@ -1,7 +1,7 @@ -using BudgetApp.Data; +using BudgetApp.Data.Entities; +using BudgetApp.Data.Repositories; using BudgetApp.Services; using BudgetApp.Tests.Helpers; -using Microsoft.EntityFrameworkCore; using Xunit; namespace BudgetApp.Tests.Services; @@ -181,9 +181,9 @@ public class DashboardServiceTests } using var context = factory.CreateContext(); - var transactionService = new TransactionService(context); - var accountService = new AccountService(context); - var categoryService = new CategoryService(context); + var transactionService = new TransactionRepository(context); + var accountService = new AccountRepository(context); + var categoryService = new CategoryRepository(context); var dashboardService = new DashboardService(transactionService, accountService, categoryService); var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId); @@ -214,9 +214,9 @@ public class DashboardServiceTests } using var context = factory.CreateContext(); - var transactionService = new TransactionService(context); - var accountService = new AccountService(context); - var categoryService = new CategoryService(context); + var transactionService = new TransactionRepository(context); + var accountService = new AccountRepository(context); + var categoryService = new CategoryRepository(context); var dashboardService = new DashboardService(transactionService, accountService, categoryService); var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId); @@ -243,9 +243,9 @@ public class DashboardServiceTests } using var context = factory.CreateContext(); - var transactionService = new TransactionService(context); - var accountService = new AccountService(context); - var categoryService = new CategoryService(context); + var transactionService = new TransactionRepository(context); + var accountService = new AccountRepository(context); + var categoryService = new CategoryRepository(context); var dashboardService = new DashboardService(transactionService, accountService, categoryService); var from = DateTime.Today.AddDays(-10); diff --git a/BudgetApp.Tests/Services/TransactionServiceTests.cs b/BudgetApp.Tests/Services/TransactionServiceTests.cs index 987936e..b1b4156 100644 --- a/BudgetApp.Tests/Services/TransactionServiceTests.cs +++ b/BudgetApp.Tests/Services/TransactionServiceTests.cs @@ -1,5 +1,6 @@ using BudgetApp.Data; -using BudgetApp.Services; +using BudgetApp.Data.Entities; +using BudgetApp.Data.Repositories; using BudgetApp.Tests.Helpers; using Microsoft.EntityFrameworkCore; using Xunit; @@ -32,7 +33,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Equal(2, result.Count); @@ -55,7 +56,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Single(result); @@ -85,7 +86,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Single(result); @@ -113,7 +114,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 10); Assert.Equal(2, result.Count); @@ -141,7 +142,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 3); Assert.Equal(3, result.Count); @@ -166,7 +167,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetUpcomingTransactionsAsync(TestDbContextFactory.TestUserId, 10); Assert.Single(result); @@ -197,7 +198,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsInRangeAsync(TestDbContextFactory.TestUserId, from, to); Assert.Equal(3, result.Count); @@ -229,7 +230,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.TestUserId); Assert.Single(result); @@ -250,7 +251,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.OtherUserId); Assert.Empty(result); @@ -271,7 +272,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "Groceries", @@ -296,7 +297,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "One-off", @@ -323,7 +324,7 @@ public class TransactionServiceTests var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "Recurring", @@ -353,7 +354,7 @@ public class TransactionServiceTests var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "Recurring", @@ -383,7 +384,7 @@ public class TransactionServiceTests var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "Weekly", @@ -421,7 +422,7 @@ public class TransactionServiceTests var startDate = new DateTime(2025, 1, 15); using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.CreateTransactionAsync(new Transaction { Description = "Monthly", @@ -481,7 +482,7 @@ public class TransactionServiceTests var newDate = DateTime.Today.AddDays(5); using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.UpdateTransactionAsync( transactionId, TestDbContextFactory.TestUserId, "Updated", 99m, newDate, TransactionType.Income, @@ -520,7 +521,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.UpdateTransactionAsync( transactionId, TestDbContextFactory.OtherUserId, "Hacked", 999m, DateTime.Today, TransactionType.Income, 1, null); @@ -548,7 +549,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -570,7 +571,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); @@ -623,7 +624,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -672,7 +673,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -709,7 +710,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionSeriesAsync(parentId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -731,7 +732,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.DeleteTransactionSeriesAsync(transactionId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); @@ -772,7 +773,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(100)); using var verifyCtx = factory.CreateContext(); @@ -808,7 +809,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(365)); using var verifyCtx = factory.CreateContext(); @@ -839,7 +840,7 @@ public class TransactionServiceTests } using var context = factory.CreateContext(); - var service = new TransactionService(context); + var service = new TransactionRepository(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); @@ -860,7 +861,7 @@ public class TransactionServiceTests var current = new DateTime(2025, 6, 15); var expected = current.AddDays(expectedDays).AddMonths(expectedMonths).AddYears(expectedYears); - var result = TransactionService.GetNextOccurrence(current, type); + var result = TransactionRepository.GetNextOccurrence(current, type); Assert.Equal(expected, result); } @@ -868,7 +869,7 @@ public class TransactionServiceTests [Fact] public void GetNextOccurrence_None_ReturnsMaxValue() { - var result = TransactionService.GetNextOccurrence(DateTime.Today, RecurrenceType.None); + var result = TransactionRepository.GetNextOccurrence(DateTime.Today, RecurrenceType.None); Assert.Equal(DateTime.MaxValue, result); } diff --git a/BudgetApp.slnx b/BudgetApp.slnx index b4a26d2..828cb22 100644 --- a/BudgetApp.slnx +++ b/BudgetApp.slnx @@ -1,4 +1,5 @@ + diff --git a/BudgetApp/BudgetApp.csproj b/BudgetApp/BudgetApp.csproj index 3cba5e6..c3894b4 100644 --- a/BudgetApp/BudgetApp.csproj +++ b/BudgetApp/BudgetApp.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -12,15 +12,15 @@ + + + + - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/BudgetApp/Components/Layout/NavMenu.razor b/BudgetApp/Components/Layout/NavMenu.razor index a8a16b8..3161194 100644 --- a/BudgetApp/Components/Layout/NavMenu.razor +++ b/BudgetApp/Components/Layout/NavMenu.razor @@ -1,4 +1,4 @@ -@inject AccountService AccountService +@inject IAccountRepository AccountService @inject AuthenticationStateProvider AuthStateProvider @inject AppState AppState @inject NavigationManager Navigation @@ -70,11 +70,11 @@ - @if (accounts is not null && accounts.Count > 0) + @if (_accounts is not null && _accounts.Count > 0) { - -@code { - private List? accounts; - - protected override async Task OnInitializedAsync() - { - AppState.OnAccountsChanged += HandleAccountsChanged; - - var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - if (userId is not null) - { - accounts = await AccountService.GetAccountsAsync(userId); - } - } - - private async void HandleAccountsChanged() - { - var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - if (userId is not null) - { - accounts = await AccountService.GetAccountsAsync(userId); - await InvokeAsync(StateHasChanged); - } - } - - public void Dispose() - { - AppState.OnAccountsChanged -= HandleAccountsChanged; - } - - private void NavigateToAccounts() - { - Navigation.NavigateTo("accounts"); - } -} - + \ No newline at end of file diff --git a/BudgetApp/Components/Layout/NavMenu.razor.cs b/BudgetApp/Components/Layout/NavMenu.razor.cs new file mode 100644 index 0000000..6cbb597 --- /dev/null +++ b/BudgetApp/Components/Layout/NavMenu.razor.cs @@ -0,0 +1,43 @@ +using BudgetApp.Data.Entities; + +namespace BudgetApp.Components.Layout; + +public partial class NavMenu +{ + private List? _accounts; + + protected override async Task OnInitializedAsync() + { + AppState.OnAccountsChanged += HandleAccountsChanged; + + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (userId is not null) + { + _accounts = await AccountService.GetAccountsAsync(userId); + } + } + + private async void HandleAccountsChanged() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (userId is not null) + { + _accounts = await AccountService.GetAccountsAsync(userId); + await InvokeAsync(StateHasChanged); + } + } + + public void Dispose() + { + AppState.OnAccountsChanged -= HandleAccountsChanged; + } + + private void NavigateToAccounts() + { + Navigation.NavigateTo("accounts"); + } +} \ No newline at end of file diff --git a/BudgetApp/Components/Pages/About.razor b/BudgetApp/Components/Pages/About.razor index 2f4bc8e..08e91bd 100644 --- a/BudgetApp/Components/Pages/About.razor +++ b/BudgetApp/Components/Pages/About.razor @@ -10,10 +10,10 @@ A personal finance dashboard built with Blazor Server.

-@if (links.Count > 0) +@if (_links.Count > 0) {
- @foreach (var link in links) + @foreach (var link in _links) { @((MarkupString)link.Icon) @link.Label @@ -24,34 +24,4 @@

Sign in or Register -

- -@code { - private record ContactLink(string Label, string Url, string Icon); - private List links = []; - - protected override void OnInitialized() - { - var about = Configuration.GetSection("About"); - - var email = about["Email"]; - if (!string.IsNullOrWhiteSpace(email)) - links.Add(new("Email", $"mailto:{email}", "")); - - var discord = about["Discord"]; - if (!string.IsNullOrWhiteSpace(discord)) - links.Add(new("Discord", discord, "")); - - var github = about["GitHub"]; - if (!string.IsNullOrWhiteSpace(github)) - links.Add(new("GitHub", github, "")); - - var twitter = about["Twitter"]; - if (!string.IsNullOrWhiteSpace(twitter)) - links.Add(new("Twitter", twitter, "")); - - var mastodon = about["Mastodon"]; - if (!string.IsNullOrWhiteSpace(mastodon)) - links.Add(new("Mastodon", mastodon, "")); - } -} +

\ No newline at end of file diff --git a/BudgetApp/Components/Pages/About.razor.cs b/BudgetApp/Components/Pages/About.razor.cs new file mode 100644 index 0000000..58db54f --- /dev/null +++ b/BudgetApp/Components/Pages/About.razor.cs @@ -0,0 +1,38 @@ +namespace BudgetApp.Components.Pages; + +public partial class About +{ + private record ContactLink(string Label, string Url, string Icon); + + private readonly List _links = []; + + protected override void OnInitialized() + { + var about = Configuration.GetSection("About"); + + var email = about["Email"]; + if (!string.IsNullOrWhiteSpace(email)) + _links.Add(new("Email", $"mailto:{email}", + "")); + + var discord = about["Discord"]; + if (!string.IsNullOrWhiteSpace(discord)) + _links.Add(new("Discord", discord, + "")); + + var github = about["GitHub"]; + if (!string.IsNullOrWhiteSpace(github)) + _links.Add(new("GitHub", github, + "")); + + var twitter = about["Twitter"]; + if (!string.IsNullOrWhiteSpace(twitter)) + _links.Add(new("Twitter", twitter, + "")); + + var mastodon = about["Mastodon"]; + if (!string.IsNullOrWhiteSpace(mastodon)) + _links.Add(new("Mastodon", mastodon, + "")); + } +} \ No newline at end of file diff --git a/BudgetApp/Components/Pages/Accounts.razor b/BudgetApp/Components/Pages/Accounts.razor index 05d37c1..ebbc3c6 100644 --- a/BudgetApp/Components/Pages/Accounts.razor +++ b/BudgetApp/Components/Pages/Accounts.razor @@ -1,6 +1,7 @@ @page "/accounts" +@using BudgetApp.Data.Entities @attribute [Authorize] -@inject AccountService AccountService +@inject IAccountRepository AccountService @inject AuthenticationStateProvider AuthStateProvider @inject AppState AppState @rendermode InteractiveServer @@ -15,20 +16,20 @@
Add Account
- +
- +
- +
- + @foreach (var kvp in Account.CurrencySymbols) { @@ -37,7 +38,7 @@
- +
@@ -47,31 +48,31 @@
-@if (accounts is null) +@if (_accounts is null) {

Loading...

} -else if (accounts.Count == 0) +else if (_accounts.Count == 0) {
No accounts yet. Add one above to get started.
} else {
- @foreach (var account in accounts) + @foreach (var account in _accounts) {
- @if (editingAccountId == account.Id) + @if (_editingAccountId == account.Id) {
- +
- @foreach (var kvp in Account.CurrencySymbols) { @@ -80,7 +81,7 @@ else
- +
@@ -100,13 +101,13 @@ else
- @if (editingBalanceAccountId == account.Id) + @if (_editingBalanceAccountId == account.Id) {
@account.GetSymbol() + @bind="_editingBalanceValue" />
@@ -131,7 +132,7 @@ else
} -@if (showDeleteModal && pendingDeleteAccount is not null) +@if (_showDeleteModal && _pendingDeleteAccount is not null) {