From 9044f3f3d7030f2d01b36045b925c26d80320a41 Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 15 Dec 2020 13:12:37 +0000 Subject: [PATCH] Merge changes for version 1.3 --- .gitignore | 6 +- CHANGELOG.md | 5 + OpenBudgeteer.Blazor/Dockerfile | 2 + .../OpenBudgeteer.Blazor.csproj | 29 +- OpenBudgeteer.Blazor/Pages/Account.razor | 12 +- OpenBudgeteer.Blazor/Pages/Bucket.razor | 12 +- OpenBudgeteer.Blazor/Pages/Import.razor | 18 +- OpenBudgeteer.Blazor/Pages/Report.razor | 2 +- OpenBudgeteer.Blazor/Pages/Rules.razor | 10 +- OpenBudgeteer.Blazor/Pages/Transaction.razor | 10 +- OpenBudgeteer.Blazor/Shared/NavMenu.razor | 4 +- OpenBudgeteer.Blazor/Startup.cs | 47 +- .../ViewModels/BlazorReportViewModel.cs | 2 +- OpenBudgeteer.Blazor/appsettings.json | 3 +- OpenBudgeteer.Core.Test/DbConnector.cs | 49 + .../OpenBudgeteer.Core.Test.csproj | 24 + .../AccountViewModelIsolatedTest.cs | 47 + .../ViewModelTest/AccountViewModelTest.cs | 71 ++ .../BucketViewModelIsolatedTest.cs | 142 +++ .../ViewModelTest/BucketViewModelTest.cs | 895 ++++++++++++++++++ .../YearMonthSelectorViewModelTest.cs | 123 +++ .../Common/{ => Database}/DatabaseContext.cs | 4 +- .../Common/Database/MySqlDatabaseContext.cs | 15 + .../Database/MySqlDatabaseContextFactory.cs | 39 + .../Common/Database/SqliteDatabaseContext.cs | 15 + .../Database/SqliteDatabaseContextFactory.cs | 29 + .../Common/ViewModelOperationResult.cs | 29 + .../20200605093534_InitialCreate.Designer.cs | 6 +- .../MySql}/20200605093534_InitialCreate.cs | 2 +- ...0200608152707_DecimalPrecision.Designer.cs | 6 +- .../MySql}/20200608152707_DecimalPrecision.cs | 2 +- ...00612082229_FixedBucketVersion.Designer.cs | 6 +- .../20200612082229_FixedBucketVersion.cs | 2 +- ...04_DefaultIncomeTransferBucket.Designer.cs | 6 +- ...00622121904_DefaultIncomeTransferBucket.cs | 2 +- .../20200701071320_BucketColor.Designer.cs | 6 +- .../MySql}/20200701071320_BucketColor.cs | 2 +- ...0200701133458_DateTimeDataType.Designer.cs | 6 +- .../MySql}/20200701133458_DateTimeDataType.cs | 2 +- ...4081215_ImportProfileDelimiter.Designer.cs | 6 +- .../20200704081215_ImportProfileDelimiter.cs | 2 +- ..._ImportProfileDateNumberFormat.Designer.cs | 6 +- ...707141613_ImportProfileDateNumberFormat.cs | 2 +- .../20200723111131_BucketNotes.Designer.cs | 6 +- .../MySql}/20200723111131_BucketNotes.cs | 2 +- ...2402_AutomaticBucketAssignment.Designer.cs | 6 +- ...0200822152402_AutomaticBucketAssignment.cs | 2 +- .../MySql}/DatabaseServiceModelSnapshot.cs | 6 +- .../20200925091603_InitialCreate.Designer.cs | 275 ++++++ .../Sqlite/20200925091603_InitialCreate.cs | 211 +++++ ...200925091907_AddInitialRecords.Designer.cs | 275 ++++++ .../20200925091907_AddInitialRecords.cs | 33 + .../SqliteDatabaseContextModelSnapshot.cs | 273 ++++++ OpenBudgeteer.Core/Models/BucketVersion.cs | 4 +- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 51 + .../OpenBudgeteer.Core.projitems | 45 - OpenBudgeteer.Core/OpenBudgeteer.Core.shproj | 13 - .../ViewModels/AccountViewModel.cs | 27 +- .../ViewModels/BucketViewModel.cs | 173 +++- .../ViewModels/ImportDataViewModel.cs | 241 +++-- .../ItemViewModels/AccountViewModelItem.cs | 63 +- .../BucketGroupViewModelItem.cs | 96 +- .../ItemViewModels/BucketViewModelItem.cs | 327 ++++--- .../MappingRuleViewModelItem.cs | 20 +- ...onthlyBucketExpensesReportViewModelItem.cs | 12 + .../PartialBucketViewModelItem.cs | 58 +- .../ItemViewModels/RuleSetViewModelItem.cs | 66 +- .../TransactionViewModelItem.cs | 224 +++-- .../ViewModels/ReportViewModel.cs | 52 +- .../ViewModels/RulesViewModel.cs | 107 ++- .../ViewModels/TransactionViewModel.cs | 103 +- .../ViewModels/YearMonthSelectorViewModel.cs | 60 +- OpenBudgeteer.sln | 18 +- README.md | 14 +- 74 files changed, 3940 insertions(+), 631 deletions(-) create mode 100644 OpenBudgeteer.Core.Test/DbConnector.cs create mode 100644 OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs rename OpenBudgeteer.Core/Common/{ => Database}/DatabaseContext.cs (99%) create mode 100644 OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs create mode 100644 OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs create mode 100644 OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs create mode 100644 OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs create mode 100644 OpenBudgeteer.Core/Common/ViewModelOperationResult.cs rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200605093534_InitialCreate.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200605093534_InitialCreate.cs (99%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200608152707_DecimalPrecision.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200608152707_DecimalPrecision.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200612082229_FixedBucketVersion.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200612082229_FixedBucketVersion.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200622121904_DefaultIncomeTransferBucket.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200622121904_DefaultIncomeTransferBucket.cs (96%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701071320_BucketColor.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701071320_BucketColor.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701133458_DateTimeDataType.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701133458_DateTimeDataType.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200704081215_ImportProfileDelimiter.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200704081215_ImportProfileDelimiter.cs (95%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200707141613_ImportProfileDateNumberFormat.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200707141613_ImportProfileDateNumberFormat.cs (95%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200723111131_BucketNotes.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200723111131_BucketNotes.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200822152402_AutomaticBucketAssignment.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200822152402_AutomaticBucketAssignment.cs (97%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/DatabaseServiceModelSnapshot.cs (98%) create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs create mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.csproj delete mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.projitems delete mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.shproj diff --git a/.gitignore b/.gitignore index 4ce6fdd..99229ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Custom files +*.db +*.DS_Store + # User-specific files *.rsuser *.suo @@ -337,4 +341,4 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e5b0c..ff6a6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 1.3 (2020-12-15) + +* [Add] Support for Sqlite databases #2 +* [Add] Unit Tests (not full coverage yet) + ### 1.2.1 (2020-12-14) * [Fixed] Crash on Report Page due to wrong DateTime creation diff --git a/OpenBudgeteer.Blazor/Dockerfile b/OpenBudgeteer.Blazor/Dockerfile index 03e093a..025f14e 100644 --- a/OpenBudgeteer.Blazor/Dockerfile +++ b/OpenBudgeteer.Blazor/Dockerfile @@ -8,6 +8,7 @@ EXPOSE 443 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build WORKDIR /src COPY ["OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj", "OpenBudgeteer.Blazor/"] +COPY ["OpenBudgeteer.Core/OpenBudgeteer.Core.csproj", "OpenBudgeteer.Core/"] RUN dotnet restore "OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj" COPY . . WORKDIR "/src/OpenBudgeteer.Blazor" @@ -19,4 +20,5 @@ RUN dotnet publish "OpenBudgeteer.Blazor.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . +RUN mkdir database ENTRYPOINT ["dotnet", "OpenBudgeteer.dll"] \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index 5cc9e1b..9755817 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -7,38 +7,21 @@ OpenBudgeteer - - - - - - - - - - - - - - - - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + + diff --git a/OpenBudgeteer.Blazor/Pages/Account.razor b/OpenBudgeteer.Blazor/Pages/Account.razor index 551a76a..b26d108 100644 --- a/OpenBudgeteer.Blazor/Pages/Account.razor +++ b/OpenBudgeteer.Blazor/Pages/Account.razor @@ -1,10 +1,11 @@ @page "/account" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Globalization +@using OpenBudgeteer.Core.Common @inject DbContextOptions DbContextOptions
@@ -74,7 +75,7 @@ _dataContext.LoadData(); StateHasChanged(); }; - HandleResult(new Tuple(true, "")); + HandleResult(new ViewModelOperationResult(true)); } private void CreateNewAccount() @@ -109,12 +110,11 @@ HandleResult(account.CloseAccount()); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor b/OpenBudgeteer.Blazor/Pages/Bucket.razor index a975ee9..24967f4 100644 --- a/OpenBudgeteer.Blazor/Pages/Bucket.razor +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor @@ -1,10 +1,11 @@ @page "/bucket" -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Core.ViewModels @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using Microsoft.EntityFrameworkCore @using System.Drawing @using System.Globalization +@using OpenBudgeteer.Core.Common @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -397,12 +398,11 @@ StateHasChanged(); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } @@ -410,7 +410,7 @@ void InOut_Changed(BucketViewModelItem bucket, KeyboardEventArgs args) { var result = bucket.HandleInOutInput(args.Key); - if (result.Item1) + if (result.IsSuccessful) { HandleResult(_dataContext.UpdateBalanceFigures()); StateHasChanged(); diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index d02bea7..a677eeb 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -1,10 +1,11 @@ @page "/import" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using System.IO @using System.Text +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Models @using Tewr.Blazor.FileReader @inject DbContextOptions DbContextOptions @@ -362,7 +363,7 @@ _dataContext.SelectedImportProfile = _dataContext.AvailableImportProfiles.First(); } var result = _dataContext.LoadProfile(); - if (result.Item1) + if (result.IsSuccessful) { _step3Enabled = _dataContext.SelectedImportProfile.ImportProfileId != 0; CheckColumnMapping(); @@ -377,7 +378,7 @@ void LoadHeaders() { var result = _dataContext.LoadHeaders(); - if (result.Item1) + if (result.IsSuccessful) { _step3Enabled = true; } @@ -404,13 +405,13 @@ void ValidateData() { - _validationErrorMessage = _dataContext.ValidateData(); + _validationErrorMessage = _dataContext.ValidateData().Message; } void ImportData() { var result = _dataContext.ImportData(); - _importConfirmationMessage = result.Item2; + _importConfirmationMessage = result.Message; _isConfirmationModalDialogVisible = true; } @@ -457,12 +458,11 @@ CheckColumnMapping(); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index bdfc702..9b8f833 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -9,7 +9,7 @@ @using ChartJs.Blazor.ChartJS.Common.Properties @using ChartJs.Blazor.Util @using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Blazor.ViewModels @inject DbContextOptions DbContextOptions diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor b/OpenBudgeteer.Blazor/Pages/Rules.razor index 80a0349..1d9febd 100644 --- a/OpenBudgeteer.Blazor/Pages/Rules.razor +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor @@ -1,10 +1,11 @@ @page "/rules" @using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Core.ViewModels @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Drawing +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Models @inject DbContextOptions DbContextOptions @@ -305,12 +306,11 @@ HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted)); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index ec6a3a2..a14e089 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -1,9 +1,10 @@ @page "/transaction" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using System.Drawing +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.ViewModels.ItemViewModels @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -292,12 +293,11 @@ HandleResult(_transactionToBeDeleted.DeleteItem()); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index fc88744..8fa9cad 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -1,4 +1,4 @@ -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @inject DbContextOptions DbContextOptions @@ -54,7 +54,7 @@
diff --git a/OpenBudgeteer.Blazor/Startup.cs b/OpenBudgeteer.Blazor/Startup.cs index f25753f..ef2363d 100644 --- a/OpenBudgeteer.Blazor/Startup.cs +++ b/OpenBudgeteer.Blazor/Startup.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.ViewModels; using Tewr.Blazor.FileReader; @@ -36,20 +36,52 @@ public void ConfigureServices(IServiceCollection services) services.AddFileReaderService(); services.AddScoped(); var configurationSection = Configuration.GetSection("Connection"); - var connectionString = $"Server={configurationSection?["Server"]};" + + var provider = configurationSection?["Provider"]; + string connectionString; + switch (provider) + { + case "mysql": + connectionString = $"Server={configurationSection?["Server"]};" + $"Port={configurationSection?["Port"]};" + $"Database={configurationSection?["Database"]};" + $"User={configurationSection?["User"]};" + $"Password={configurationSection?["Password"]}"; - services.AddDbContext(options => options.UseMySql( - connectionString, - b => b.MigrationsAssembly("OpenBudgeteer")), - ServiceLifetime.Transient); + + services.AddDbContext(options => options.UseMySql( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); + + // Check on Pending Db Migrations + var mySqlDbContext = new MySqlDatabaseContextFactory().CreateDbContext(Configuration); + if (mySqlDbContext.Database.GetPendingMigrations().Any()) mySqlDbContext.Database.Migrate(); + + break; + case "sqlite": +#if DEBUG + connectionString = "Data Source=openbudgeteer.db"; +#else + connectionString = "Data Source=database/openbudgeteer.db"; +#endif + services.AddDbContext(options => options.UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); + + // Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) sqliteDbContext.Database.Migrate(); + + break; + default: + throw new ArgumentOutOfRangeException($"Database provider {provider} not supported"); + } + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Required to read ANSI Text files } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseContext dbContext) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -73,7 +105,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Database endpoints.MapFallbackToPage("/_Host"); }); - if (dbContext.Database.GetPendingMigrations().Any()) dbContext.Database.Migrate(); // TODO Get Culture from Settings var cultureInfo = new CultureInfo("de-DE"); CultureInfo.DefaultThreadCurrentCulture = cultureInfo; diff --git a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs index f0d2633..2c29efe 100644 --- a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs +++ b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs @@ -14,7 +14,7 @@ using ChartJs.Blazor.ChartJS.LineChart; using ChartJs.Blazor.Util; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.ViewModels; namespace OpenBudgeteer.Blazor.ViewModels diff --git a/OpenBudgeteer.Blazor/appsettings.json b/OpenBudgeteer.Blazor/appsettings.json index 1dfa011..1c937d5 100644 --- a/OpenBudgeteer.Blazor/appsettings.json +++ b/OpenBudgeteer.Blazor/appsettings.json @@ -1,10 +1,11 @@ { "Connection": { + "Provider" : "mysql", "Database": "openbudgeteer-dev", "Server": "cl4p-tp", "Port": "3306", "User": "openbudgeteer-dev", - "Password": "openbudgeteer-dev" + "Password": "openbudgeteer-dev" }, "Logging": { "LogLevel": { diff --git a/OpenBudgeteer.Core.Test/DbConnector.cs b/OpenBudgeteer.Core.Test/DbConnector.cs new file mode 100644 index 0000000..dd7bd95 --- /dev/null +++ b/OpenBudgeteer.Core.Test/DbConnector.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Test +{ + public class DbConnector + { + public static DbContextOptions GetDbContextOptions(string dbName) + { + //var connectionString = "Server=cl4p-tp;" + + // "Port=3306;" + + // "Database=openbudgeteer-test;" + + // "User=openbudgeteer-test;" + + // "Password=openbudgeteer-test"; + //return new DbContextOptionsBuilder() + // .UseMySql(connectionString) + // .Options; + + var connectionString = $"Data Source={dbName}.db"; + + //Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) + sqliteDbContext.Database.Migrate(); + + return new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .Options; + } + + public static void CleanupDatabase(string dbName) + { + using (var dbContext = new DatabaseContext(GetDbContextOptions(dbName))) + { + dbContext.DeleteAccounts(dbContext.Account); + dbContext.DeleteBankTransactions(dbContext.BankTransaction); + dbContext.DeleteBuckets(dbContext.Bucket); + dbContext.DeleteBucketGroups(dbContext.BucketGroup); + dbContext.DeleteBucketMovements(dbContext.BucketMovement); + dbContext.DeleteBucketRuleSets(dbContext.BucketRuleSet); + dbContext.DeleteBucketVersions(dbContext.BucketVersion); + dbContext.DeleteBudgetedTransactions(dbContext.BudgetedTransaction); + dbContext.DeleteImportProfiles(dbContext.ImportProfile); + dbContext.DeleteMappingRules(dbContext.MappingRule); + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj new file mode 100644 index 0000000..ae4a64b --- /dev/null +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs new file mode 100644 index 0000000..000a66a --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + [CollectionDefinition("AccountViewModelIsolatedTest", DisableParallelization = true)] + public class AccountViewModelIsolatedTest + { + private readonly DbContextOptions _dbOptions; + + public AccountViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelIsolatedTest)); + } + + [Fact] + public void LoadData_CheckNameAndLoadOnlyActiveAccounts() + { + DbConnector.CleanupDatabase(nameof(AccountViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateAccounts(new[] + { + new Account() {Name = "Test Account1", IsActive = 1}, + new Account() {Name = "Test Account2", IsActive = 1}, + new Account() {Name = "Test Account3", IsActive = 0} + }); + } + + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); + + Assert.Equal(2, viewModel.Accounts.Count); + + var testItem1 = viewModel.Accounts.ElementAt(0); + var testItem2 = viewModel.Accounts.ElementAt(1); + + Assert.Equal("Test Account1", testItem1.Account.Name); + Assert.Equal("Test Account2", testItem2.Account.Name); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs new file mode 100644 index 0000000..98e485e --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class AccountViewModelTest + { + private readonly DbContextOptions _dbOptions; + + public AccountViewModelTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelTest)); + } + + public static IEnumerable TestData_LoadData_CheckTransactionCalculations + { + get + { + return new[] + { + new object[] {new List {12.34m, -12.34m, 12.34m}, 12.34m, 24.68m, -12.34m}, + new object[] {new List {0}, 0, 0, 0} + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadData_CheckTransactionCalculations))] + public void LoadData_CheckTransactionCalculations( + List transactionAmounts, + decimal expectedBalance, + decimal expectedIn, + decimal expectedOut) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() {Name = "Test Account", IsActive = 1}; + dbContext.CreateAccount(testAccount); + + foreach (var transactionAmount in transactionAmounts) + { + dbContext.CreateBankTransaction( + new BankTransaction() + { + AccountId = testAccount.AccountId, + TransactionDate = DateTime.Now, + Amount = transactionAmount + } + ); + } + + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); + var testItem1 = viewModel.Accounts + .FirstOrDefault(i => i.Account.AccountId == testAccount.AccountId); + + Assert.NotNull(testItem1); + Assert.Equal(expectedBalance, testItem1.Balance); + Assert.Equal(expectedIn, testItem1.In); + Assert.Equal(expectedOut, testItem1.Out); + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs new file mode 100644 index 0000000..3b7c72e --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs @@ -0,0 +1,142 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + [CollectionDefinition("BucketViewModelIsolatedTest", DisableParallelization = true)] + public class BucketViewModelIsolatedTest + { + private readonly DbContextOptions _dbOptions; + + public BucketViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelIsolatedTest)); + } + + [Fact] + public async Task LoadDataAsync_CheckBucketGroupsNamesAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + Assert.Equal(3, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + } + + [Fact] + public async Task CreateGroup_CheckGroupCreationAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1 }, + new BucketGroup() { Name = "Bucket Group 2", Position = 3 }, + new BucketGroup() { Name = "Bucket Group 3", Position = 2 } + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var result = viewModel.CreateGroup(); + + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.True(viewModel.BucketGroups.First().InModification); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); + + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.False(viewModel.BucketGroups.First().InModification); + } + + [Fact] + public async Task DeleteGroup_CheckGroupDeletionAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var groupToDelete = viewModel.BucketGroups.ElementAt(1); + var result = viewModel.DeleteGroup(groupToDelete); + + Assert.True(result.IsSuccessful); + Assert.True(result.ViewModelReloadInvoked); + + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs new file mode 100644 index 0000000..de7ce3c --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -0,0 +1,895 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using OpenBudgeteer.Core.ViewModels.ItemViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class BucketViewModelTest + { + private readonly DbContextOptions _dbOptions; + + public BucketViewModelTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelTest)); + } + + public static IEnumerable TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets + { + get + { + return new[] + { + new object[] {new List {"Bucket 1"}}, + new object[] {new List {"Bucket 1", "Bucket 2", "Bucket 3"}}, + new object[] {new List()}, + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets))] + public async Task LoadDataAsync_CheckBucketGroupAssignedBuckets(List bucketNames) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNames) + { + dbContext.CreateBucket(new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var testObject = viewModel.BucketGroups + .FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + + Assert.NotNull(testObject); + Assert.Equal(bucketNames.Count, testObject.Buckets.Count); + foreach (var bucketName in bucketNames) + { + Assert.Contains(testObject.Buckets, i => i.Bucket.Name == bucketName); + } + } + } + + public static IEnumerable TestData_LoadDataAsync_CheckBucketSorting + { + get + { + return new[] + { + new object[] {new List {"A_Bucket 1", "C_Bucket 2", "B_Bucket 3"}, new List {"A_Bucket 1", "B_Bucket 3", "C_Bucket 2"} } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketSorting))] + public async Task LoadDataAsync_CheckBucketSorting(List bucketNamesUnsorted, List expectedBucketNamesSorted) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNamesUnsorted) + { + dbContext.CreateBucket(new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + Assert.Equal(bucketNamesUnsorted.Count, bucketGroup.Buckets.Count); + + for (int i = 0; i < bucketGroup.Buckets.Count; i++) + { + Assert.Equal( + expectedBucketNamesSorted.ElementAt(i), + bucketGroup.Buckets.ElementAt(i).Bucket.Name); + } + } + } + + public static IEnumerable TestData_LoadDataAsync_LoadOnlyActiveBuckets + { + get + { + return new[] + { + // Active in current month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), false, null, true}, + // Active starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), false, null, false}, + // Active starting next year + new object[] { new DateTime(2010,1,1), new DateTime(2011,1,1), false, null, false}, + // Inactive since current month + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive since last year + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2009,1,1), false}, + // Inactive since last month + new object[] { new DateTime(2010,2,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), true, new DateTime(2010,2,1), true}, + // Active starting next month but already inactive in the future + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), true, new DateTime(2010,3,1), false} + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_LoadOnlyActiveBuckets))] + public async Task LoadDataAsync_LoadOnlyActiveBuckets( + DateTime testMonth, + DateTime bucketActiveSince, + bool bucketIsInactive, + DateTime bucketIsInActiveFrom, + bool expectedBucketAvailable + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() {IsActive = 1, Name = "Account"}; + var testBucketGroup = new BucketGroup() {Name = "Bucket Group", Position = 1}; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket", + ValidFrom = bucketActiveSince, + IsInactive = bucketIsInactive, + IsInactiveFrom = bucketIsInActiveFrom, + }; + + dbContext.CreateBucket(testBucket); + dbContext.CreateBucketVersion(new BucketVersion() + { + BucketId = testBucket.BucketId, + Version = 1, + BucketType = 1, + ValidFrom = bucketActiveSince + }); + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + Assert.Equal(expectedBucketAvailable, bucketGroup.Buckets.Any()); + } + } + + [Fact] + public async Task LoadDataAsync_CheckValidFromHandling() + { + try + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Current Month", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Past", + ValidFrom = new DateTime(2009, 1, 1) + }; + var testBucket3 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Future", + ValidFrom = new DateTime(2010, 2, 1) + }; + + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2, testBucket3 + }); + dbContext.CreateBucketVersions(new[] + { + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + new BucketVersion() { BucketId = testBucket3.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket3.ValidFrom }, + }); + + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + Assert.Equal(2, bucketGroup.Buckets.Count); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.DoesNotContain(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket3.BucketId); + } + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + [Fact] + public async Task LoadDataAsync_CheckCalculatedValues() + { + try + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 1", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 2", + ValidFrom = new DateTime(2010, 1, 1) + }; + + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2 + }); + dbContext.CreateBucketVersions(new[] + { + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + }); + + var testTransactions = new[] + { + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 1 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -10 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 100 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -1000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 10000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2009,1,1), Amount = 100000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,2,1), Amount = 1000000 }, + }; + dbContext.CreateBankTransactions(testTransactions); + + dbContext.CreateBudgetedTransactions(new[] + { + new BudgetedTransaction() { TransactionId = testTransactions[0].TransactionId, BucketId = testBucket1.BucketId, Amount = 1 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket1.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket2.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[2].TransactionId, BucketId = testBucket1.BucketId, Amount = 100 }, + new BudgetedTransaction() { TransactionId = testTransactions[3].TransactionId, BucketId = testBucket2.BucketId, Amount = -1000 }, + new BudgetedTransaction() { TransactionId = testTransactions[4].TransactionId, BucketId = testBucket2.BucketId, Amount = 10000 }, + new BudgetedTransaction() { TransactionId = testTransactions[5].TransactionId, BucketId = testBucket2.BucketId, Amount = 100000 }, + new BudgetedTransaction() { TransactionId = testTransactions[6].TransactionId, BucketId = testBucket2.BucketId, Amount = 1000000 }, + }); + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + // This test includes: + // - Bucket Split + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-5, testObject.Activity); + Assert.Equal(96, testObject.Balance); + Assert.Equal(101, testObject.In); + + // This test includes: + // - Bucket Split + // - Include Transactions in previous months for Balance + // - Exclude Transactions in the future + testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-1005, testObject.Activity); + Assert.Equal(108995, testObject.Balance); + Assert.Equal(10000, testObject.In); + } + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_MonthlyExpenses + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "Bucket with pending Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List(), + 10, 0, 0 + }, + new object[] + { + new Bucket { Name = "Bucket with fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List() + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } + }, + 0, 10 ,0 + }, + new object[] + { + new Bucket { Name = "Bucket pending Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } + }, + new List(), + 10, 0, -10 + }, + new object[] + { + new Bucket { Name = "Bucket fulfilled Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } + }, + 0, 10, -10 + }, + new object[] + { + new Bucket { Name = "Bucket with partial fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 5 } + }, + 5, 5, 0 + }, + new object[] + { + new Bucket { Name = "Bucket with over fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 15 } + }, + 0, 15 ,0 + } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_MonthlyExpenses))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_MonthlyExpenses( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity + ) + { + try + { + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); + + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "120 every 12 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, without Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, without Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 70, "120 until 2010-06", 58 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } + }, + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } + }, + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, no input", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List(), + 20, 0, 0, 0, "120 until 2010-06", 0 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 } + }, + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,3,1) }, + new List(), + new List(), + 33.33m, 0, 0, 0, "100 until 2010-03", 0 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, with Want", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 33.33m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 33.33m } + }, + 33.34m, 0, 0, 66.66m, "100 until 2010-01", 67 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, input not in sync", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 12.34m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 56.78m } + }, + 30.88m, 0, 0, 69.12m, "100 until 2010-01", 69 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with expenses", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,9,2), Amount = -30 }, + new BankTransaction { TransactionDate = new DateTime(2010,1,2), Amount = -10 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 16.67m, 0, -10, 20, "120 until 2010-06", 17 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, 2nd year, last 6 months, with Want", ValidFrom = new DateTime(2008,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,6,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,6,1), Amount = -120 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2008,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,1,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,2,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,3,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,4,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,5,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,6,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + } + }; + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveXUntilY + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "120 until 2010-12, no input", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] + { + new Bucket { Name = "120 until 2010-12, input in current Month", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } + }, + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } + }, + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "120 until 2009-12, target not reached", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 90, 0, 0, 30, "120 until 2009-12", 25 + }, + new object[] + { + new Bucket { Name = "30 until 2009-12, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2009,12,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,12,1), Amount = -30 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 0, 0, 0, 0, "30 until 2009-12", 100 + } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths))] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_SaveXUntilY))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_ExpenseEveryXMonths_SaveXUntilY( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity, + decimal expectedBalance, + string expectedDetails, + int expectedProgress + ) + { + try + { + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); + Assert.Equal(expectedBalance, testObject.Balance); + Assert.Equal(expectedDetails, testObject.Details); + Assert.Equal(expectedProgress, testObject.Progress); + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + private async Task ExecuteBucketCreationAndTransactionMovementsAsync( + Bucket testBucket, + BucketVersion testBucketVersion, + IEnumerable testTransactions, + IEnumerable testBucketMovements, + DateTime testMonth) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + testBucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(testBucket); + + testBucketVersion.BucketId = testBucket.BucketId; + testBucketVersion.ValidFrom = testBucket.ValidFrom; + dbContext.CreateBucketVersion(testBucketVersion); + + foreach (var testTransaction in testTransactions) + { + testTransaction.AccountId = testAccount.AccountId; + dbContext.CreateBankTransaction(testTransaction); + dbContext.CreateBudgetedTransaction(new BudgetedTransaction() + { + TransactionId = testTransaction.TransactionId, + BucketId = testBucket.BucketId, + Amount = testTransaction.Amount + }); + } + + foreach (var testBucketMovement in testBucketMovements) + { + testBucketMovement.BucketId = testBucket.BucketId; + dbContext.CreateBucketMovement(testBucketMovement); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket.BucketId); + Assert.NotNull(testObject); + + return testObject; + } + } + + public async Task DistributeBudget_CheckDistributedMoney( + IEnumerable> testBuckets + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var (bucket, bucketVersion) in testBuckets) + { + bucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(bucket); + bucketVersion.BucketId = bucket.BucketId; + dbContext.CreateBucketVersion(bucketVersion); + } + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs new file mode 100644 index 0000000..fa19376 --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs @@ -0,0 +1,123 @@ +using System; +using System.Globalization; +using System.Linq; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class YearMonthSelectorViewModelTest + { + [Fact] + public void Constructor_CheckDefaults() + { + var viewModel = new YearMonthSelectorViewModel(); + + Assert.Equal(DateTime.Now.Year, viewModel.SelectedYear); + Assert.Equal(DateTime.Now.Month, viewModel.SelectedMonth); + Assert.Equal(new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1), viewModel.CurrentMonth); + + // Test Months + Assert.Equal(12, viewModel.Months.Count); + for (int i = 1; i < 13; i++) + { + Assert.Equal(i, viewModel.Months.ElementAt(i-1)); + } + + var cultureInfo = new CultureInfo("de-DE"); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + var converter = new MonthOutputConverter(); + Assert.Equal("Jan", converter.ConvertMonth(1)); + Assert.Equal("Feb", converter.ConvertMonth(2)); + Assert.Equal("Mrz", converter.ConvertMonth(3)); + Assert.Equal("Apr", converter.ConvertMonth(4)); + Assert.Equal("Mai", converter.ConvertMonth(5)); + Assert.Equal("Jun", converter.ConvertMonth(6)); + Assert.Equal("Jul", converter.ConvertMonth(7)); + Assert.Equal("Aug", converter.ConvertMonth(8)); + Assert.Equal("Sep", converter.ConvertMonth(9)); + Assert.Equal("Okt", converter.ConvertMonth(10)); + Assert.Equal("Nov", converter.ConvertMonth(11)); + Assert.Equal("Dez", converter.ConvertMonth(12)); + } + + [Fact] + public void PreviousMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 2 + }; + + viewModel.PreviousMonth(); + + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); + + viewModel.PreviousMonth(); + + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); + } + + [Fact] + public void NextMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2009, + SelectedMonth = 11 + }; + + viewModel.NextMonth(); + + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); + + viewModel.NextMonth(); + + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); + } + + [Fact] + public void SelectedYearMonthChanged_CheckEventHasBeenInvoked() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var eventHasBeenInvoked = false; + viewModel.SelectedYearMonthChanged += (sender, args) => eventHasBeenInvoked = true; + + viewModel.SelectedYear = 2010; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 1; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedYear = 2009; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 2; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.NextMonth(); + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.PreviousMonth(); + Assert.True(eventHasBeenInvoked); + + + } + } +} diff --git a/OpenBudgeteer.Core/Common/DatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs similarity index 99% rename from OpenBudgeteer.Core/Common/DatabaseContext.cs rename to OpenBudgeteer.Core/Common/Database/DatabaseContext.cs index d1e3f8e..1b45fe2 100644 --- a/OpenBudgeteer.Core/Common/DatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.Common +namespace OpenBudgeteer.Core.Common.Database { public class DatabaseContext : DbContext { @@ -20,7 +20,7 @@ public class DatabaseContext : DbContext public DbSet ImportProfile { get; set; } public DbSet BucketRuleSet { get; set; } public DbSet MappingRule { get; set; } - + public DatabaseContext(DbContextOptions options) : base(options) { } #region Create diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs new file mode 100644 index 0000000..d8fe386 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class MySqlDatabaseContext : DatabaseContext + { + public MySqlDatabaseContext(DbContextOptions options) : base(options) + { + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs new file mode 100644 index 0000000..06cc616 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class MySqlDatabaseContextFactory : IDesignTimeDbContextFactory + { + public MySqlDatabaseContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql("Server=192.168.178.30;" + + "Port=3306;" + + "Database=openbudgeteer-dev" + + "User=openbudgeteer-dev" + + "Password=openbudgeteer-dev"); + + return new MySqlDatabaseContext(optionsBuilder.Options); + } + + public MySqlDatabaseContext CreateDbContext(IConfiguration configuration) + { + var configurationSection = configuration.GetSection("Connection"); + var connectionString = $"Server={configurationSection?["Server"]};" + + $"Port={configurationSection?["Port"]};" + + $"Database={configurationSection?["Database"]};" + + $"User={configurationSection?["User"]};" + + $"Password={configurationSection?["Password"]}"; + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new MySqlDatabaseContext(optionsBuilder.Options); + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs new file mode 100644 index 0000000..29df075 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class SqliteDatabaseContext : DatabaseContext + { + public SqliteDatabaseContext(DbContextOptions options) : base(options) + { + + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs new file mode 100644 index 0000000..a4da3dc --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class SqliteDatabaseContextFactory : IDesignTimeDbContextFactory + { + public SqliteDatabaseContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=openbudgeteer.db"); + + return new SqliteDatabaseContext(optionsBuilder.Options); + } + + public SqliteDatabaseContext CreateDbContext(string connectionString) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new SqliteDatabaseContext(optionsBuilder.Options); + } + } +} diff --git a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs new file mode 100644 index 0000000..1683284 --- /dev/null +++ b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OpenBudgeteer.Core.Common +{ + public class ViewModelOperationResult + { + public bool IsSuccessful { get; } + public string Message { get; } + public bool ViewModelReloadInvoked { get; } + + public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadInvoked = false) + { + IsSuccessful = isSuccessful; + Message = message; + ViewModelReloadInvoked = viewModelReloadInvoked; + } + + public ViewModelOperationResult(bool isSuccessful, bool viewModelReloadInvoked = false) + : this(isSuccessful, string.Empty, viewModelReloadInvoked) + { + if (!isSuccessful) + { + Message = "Unknown Error."; + } + } + } +} diff --git a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs index f395e9c..7be612b 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200605093534_InitialCreate")] partial class InitialCreate { diff --git a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs similarity index 99% rename from OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs index a6708c8..c3a287d 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class InitialCreate : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs index 59b4f87..befe5d3 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200608152707_DecimalPrecision")] partial class DecimalPrecision { diff --git a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs index adb8f7d..2b7ec94 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DecimalPrecision : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs index 6649280..19e7e36 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200612082229_FixedBucketVersion")] partial class FixedBucketVersion { diff --git a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs index 4c802cb..ae57003 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class FixedBucketVersion : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs index d7d6e78..31f4054 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200622121904_DefaultIncomeTransferBucket")] partial class DefaultIncomeTransferBucket { diff --git a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs similarity index 96% rename from OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs index dafb128..c479046 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DefaultIncomeTransferBucket : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs index b4b93be..e7cb42f 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200701071320_BucketColor")] partial class BucketColor { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs index dce4a5b..f98dd44 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class BucketColor : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs index 7478940..7d355c2 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200701133458_DateTimeDataType")] partial class DateTimeDataType { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs index c556640..c2769fd 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DateTimeDataType : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs index 9498cd5..a8910bf 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200704081215_ImportProfileDelimiter")] partial class ImportProfileDelimiter { diff --git a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs similarity index 95% rename from OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs index 0878ef8..d4f36bc 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class ImportProfileDelimiter : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs index 9812635..4fa1438 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200707141613_ImportProfileDateNumberFormat")] partial class ImportProfileDateNumberFormat { diff --git a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs similarity index 95% rename from OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs index e836b78..9369046 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class ImportProfileDateNumberFormat : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs index 98aa4d7..6c394c9 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200723111131_BucketNotes")] partial class BucketNotes { diff --git a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs index 165f662..2f00d2c 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class BucketNotes : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs index 7a30532..4e5596b 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200822152402_AutomaticBucketAssignment")] partial class AutomaticBucketAssignment { diff --git a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs similarity index 97% rename from OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs index 4d24fe1..84b8ab5 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class AutomaticBucketAssignment : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs rename to OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs index b3deeb4..ada9233 100644 --- a/OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] partial class DatabaseServiceModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs new file mode 100644 index 0000000..f6da2a4 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20200925091603_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs new file mode 100644 index 0000000..ccadfed --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs @@ -0,0 +1,211 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Account", + columns: table => new + { + AccountId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + IsActive = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Account", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "BankTransaction", + columns: table => new + { + TransactionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(nullable: false), + TransactionDate = table.Column(nullable: false), + Payee = table.Column(nullable: true), + Memo = table.Column(nullable: true), + Amount = table.Column(type: "decimal(65, 2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BankTransaction", x => x.TransactionId); + }); + + migrationBuilder.CreateTable( + name: "Bucket", + columns: table => new + { + BucketId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + BucketGroupId = table.Column(nullable: false), + ColorCode = table.Column(nullable: true), + ValidFrom = table.Column(nullable: false), + IsInactive = table.Column(nullable: false), + IsInactiveFrom = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Bucket", x => x.BucketId); + }); + + migrationBuilder.CreateTable( + name: "BucketGroup", + columns: table => new + { + BucketGroupId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + Position = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketGroup", x => x.BucketGroupId); + }); + + migrationBuilder.CreateTable( + name: "BucketMovement", + columns: table => new + { + BucketMovementId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketId = table.Column(nullable: false), + Amount = table.Column(type: "decimal(65, 2)", nullable: false), + MovementDate = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketMovement", x => x.BucketMovementId); + }); + + migrationBuilder.CreateTable( + name: "BucketRuleSet", + columns: table => new + { + BucketRuleSetId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Priority = table.Column(nullable: false), + Name = table.Column(nullable: true), + TargetBucketId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketRuleSet", x => x.BucketRuleSetId); + }); + + migrationBuilder.CreateTable( + name: "BucketVersion", + columns: table => new + { + BucketVersionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketId = table.Column(nullable: false), + Version = table.Column(nullable: false), + BucketType = table.Column(nullable: false), + BucketTypeXParam = table.Column(nullable: false), + BucketTypeYParam = table.Column(type: "decimal(65, 2)", nullable: false), + BucketTypeZParam = table.Column(nullable: false), + Notes = table.Column(nullable: true), + ValidFrom = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketVersion", x => x.BucketVersionId); + }); + + migrationBuilder.CreateTable( + name: "BudgetedTransaction", + columns: table => new + { + BudgetedTransactionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TransactionId = table.Column(nullable: false), + BucketId = table.Column(nullable: false), + Amount = table.Column(type: "decimal(65, 2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetedTransaction", x => x.BudgetedTransactionId); + }); + + migrationBuilder.CreateTable( + name: "ImportProfile", + columns: table => new + { + ImportProfileId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProfileName = table.Column(nullable: true), + AccountId = table.Column(nullable: false), + HeaderRow = table.Column(nullable: false), + Delimiter = table.Column(nullable: false), + TextQualifier = table.Column(nullable: false), + DateFormat = table.Column(nullable: true), + NumberFormat = table.Column(nullable: true), + TransactionDateColumnName = table.Column(nullable: true), + PayeeColumnName = table.Column(nullable: true), + MemoColumnName = table.Column(nullable: true), + AmountColumnName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportProfile", x => x.ImportProfileId); + }); + + migrationBuilder.CreateTable( + name: "MappingRule", + columns: table => new + { + MappingRuleId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketRuleSetId = table.Column(nullable: false), + ComparisionField = table.Column(nullable: false), + ComparisionType = table.Column(nullable: false), + ComparisionValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MappingRule", x => x.MappingRuleId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Account"); + + migrationBuilder.DropTable( + name: "BankTransaction"); + + migrationBuilder.DropTable( + name: "Bucket"); + + migrationBuilder.DropTable( + name: "BucketGroup"); + + migrationBuilder.DropTable( + name: "BucketMovement"); + + migrationBuilder.DropTable( + name: "BucketRuleSet"); + + migrationBuilder.DropTable( + name: "BucketVersion"); + + migrationBuilder.DropTable( + name: "BudgetedTransaction"); + + migrationBuilder.DropTable( + name: "ImportProfile"); + + migrationBuilder.DropTable( + name: "MappingRule"); + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs new file mode 100644 index 0000000..e975505 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20200925091907_AddInitialRecords")] + partial class AddInitialRecords + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs new file mode 100644 index 0000000..f21406a --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + public partial class AddInitialRecords : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Bucket", + columns: new[] { "BucketId", "Name", "BucketGroupId", "ValidFrom", "IsInactive", "IsInactiveFrom" }, + values: new object[] { 1, "Income", 0, "1990-01-01", false, $"{DateTime.MaxValue:yyyy-MM-dd}" }); + migrationBuilder.InsertData( + table: "Bucket", + columns: new[] { "BucketId", "Name", "BucketGroupId", "ValidFrom", "IsInactive", "IsInactiveFrom" }, + values: new object[] { 2, "Transfer", 0, "1990-01-01", false, $"{DateTime.MaxValue:yyyy-MM-dd}" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Bucket", + keyColumn: "BucketId", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "Bucket", + keyColumn: "BucketId", + keyValue: 2); + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs b/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..03142f2 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs @@ -0,0 +1,273 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + partial class SqliteDatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Models/BucketVersion.cs b/OpenBudgeteer.Core/Models/BucketVersion.cs index 694e360..bf17921 100644 --- a/OpenBudgeteer.Core/Models/BucketVersion.cs +++ b/OpenBudgeteer.Core/Models/BucketVersion.cs @@ -87,7 +87,7 @@ public int BucketTypeXParam private decimal _bucketTypeYParam; /// - /// Parameter for a Amount values. For BucketType: + /// Parameter for an Amount value. For BucketType: /// /// 1 - 0
/// 2-3 - decimal Amount
@@ -103,7 +103,7 @@ public decimal BucketTypeYParam private DateTime _bucketTypeZParam; /// - /// Parameter for date values. For BucketType: + /// Parameter for target date value. For BucketType: /// /// 1-2 - string.Empty
/// 3 - DateTime First target date
diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj new file mode 100644 index 0000000..47678e3 --- /dev/null +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -0,0 +1,51 @@ + + + + netcoreapp3.1 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + 20200605093534_InitialCreate.cs + + + 20200608152707_DecimalPrecision.cs + + + 20200612082229_FixedBucketVersion.cs + + + 20200622121904_DefaultIncomeTransferBucket.cs + + + 20200701071320_BucketColor.cs + + + 20200701133458_DateTimeDataType.cs + + + 20200704081215_ImportProfileDelimiter.cs + + + 20200707141613_ImportProfileDateNumberFormat.cs + + + 20200723111131_BucketNotes.cs + + + 20200822152402_AutomaticBucketAssignment.cs + + + + diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems b/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems deleted file mode 100644 index 87ac238..0000000 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems +++ /dev/null @@ -1,45 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - cc22a1e2-fa88-417c-a402-69526c33d2f4 - - - OpenBudgeteer.Core - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj deleted file mode 100644 index 6ae6599..0000000 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - cc22a1e2-fa88-417c-a402-69526c33d2f4 - 14.0 - - - - - - - - diff --git a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs index 408834f..9704d65 100644 --- a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Windows; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -16,29 +16,45 @@ namespace OpenBudgeteer.Core.ViewModels public class AccountViewModel : ViewModelBase { private ObservableCollection _accounts; + /// + /// Collection of ViewModelItems for Model + /// public ObservableCollection Accounts { get => _accounts; set => Set(ref _accounts, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public AccountViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; Accounts = new ObservableCollection(); } + /// + /// Initialize ViewModel and load data from database + /// public void LoadData() { Accounts.Clear(); using (var accountDbContext = new DatabaseContext(_dbOptions)) { - foreach (var account in accountDbContext.Account.Where(i => i.IsActive == 1).OrderBy(i => i.Name)) + foreach (var account in accountDbContext.Account + .Where(i => i.IsActive == 1) + .OrderBy(i => i.Name)) { var newAccountItem = new AccountViewModelItem(_dbOptions, account); decimal newIn = 0; @@ -69,6 +85,10 @@ public void LoadData() } } + /// + /// Creates an initial which can be used for further manipulation + /// + /// Newly initialized public AccountViewModelItem PrepareNewAccount() { var result = new AccountViewModelItem(_dbOptions) @@ -83,6 +103,9 @@ public AccountViewModelItem PrepareNewAccount() return result; } + /// + /// Forces reload of ViewModel to revoke unsaved changes + /// public void CancelEditMode() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs index ae99a03..041cc1a 100644 --- a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs @@ -6,12 +6,13 @@ using System.Text; using System.Text.RegularExpressions; using System.Windows; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels @@ -19,75 +20,99 @@ namespace OpenBudgeteer.Core.ViewModels public class BucketViewModel : ViewModelBase { private decimal _income; + /// + /// Money that has been added to a Bucket + /// public decimal Income { get => _income; - set => Set(ref _income, value); + private set => Set(ref _income, value); } private decimal _expenses; + /// + /// Money that has been moved out of the Bucket + /// public decimal Expenses { get => _expenses; - set => Set(ref _expenses, value); + private set => Set(ref _expenses, value); } private decimal _monthBalance; + /// + /// Combined Income and Expenses in a specific month + /// public decimal MonthBalance { get => _monthBalance; - set => Set(ref _monthBalance, value); + private set => Set(ref _monthBalance, value); } private decimal _budget; + /// + /// Available Money in a specific month + /// public decimal Budget { get => _budget; - set => Set(ref _budget, value); + private set => Set(ref _budget, value); } private decimal _bankBalance; + /// + /// Money available on all bank accounts + /// public decimal BankBalance { get => _bankBalance; - set => Set(ref _bankBalance, value); + private set => Set(ref _bankBalance, value); } private decimal _pendingWant; + /// + /// Money expected to be added to a Bucket in a specific month + /// public decimal PendingWant { get => _pendingWant; - set => Set(ref _pendingWant, value); + private set => Set(ref _pendingWant, value); } private decimal _remainingBudget; + /// + /// Remaining Money in a specific month. Includes Want and negative Balances + /// public decimal RemainingBudget { get => _remainingBudget; - set => Set(ref _remainingBudget, value); + private set => Set(ref _remainingBudget, value); } private decimal _negativeBucketBalance; + /// + /// Sum of all Bucket Balances where the number is negative + /// public decimal NegativeBucketBalance { get => _negativeBucketBalance; - set => Set(ref _negativeBucketBalance, value); + private set => Set(ref _negativeBucketBalance, value); } private ObservableCollection _bucketGroups; + /// + /// Collection of Groups which contains a set of Buckets + /// public ObservableCollection BucketGroups { get => _bucketGroups; - set => Set(ref _bucketGroups, value); - } - - private ObservableCollection _months; - public ObservableCollection Months - { - get => _months; - set => Set(ref _months, value); + private set => Set(ref _bucketGroups, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; @@ -95,6 +120,11 @@ public ObservableCollection Months private bool _defaultCollapseState; // Keep Collapse State e.g. after YearMonth change of ViewModel reload + /// + /// Basic constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month public BucketViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) { _dbOptions = dbOptions; @@ -103,7 +133,11 @@ public BucketViewModel(DbContextOptions dbOptions, YearMonthSel //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { try { @@ -147,19 +181,23 @@ public async Task> LoadDataAsync() BucketGroups.Add(newBucketGroup); } } - var (success, errorMessage) = UpdateBalanceFigures(); - if (!success) throw new Exception(errorMessage); + var result = UpdateBalanceFigures(); + if (!result.IsSuccessful) throw new Exception(result.Message); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple CreateGroup() + /// + /// Creates an initial and adds it to ViewModel and Database. + /// Will be added on first position and updates all other Positions accordingly + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateGroup() { - var newPosition = BucketGroups.Count == 0 ? 1 : BucketGroups.Last().BucketGroup.Position + 1; var newGroup = new BucketGroup { BucketGroupId = 0, @@ -173,8 +211,7 @@ public Tuple CreateGroup() bucketGroup.BucketGroup.Position++; dbContext.UpdateBucketGroup(bucketGroup.BucketGroup); } - if (dbContext.CreateBucketGroup(newGroup) == 0) - return new Tuple(false, "Unable to write changes to database"); + if (dbContext.CreateBucketGroup(newGroup) == 0) return new ViewModelOperationResult(false, "Unable to write changes to database"); } var newBucketGroupViewModelItem = @@ -188,10 +225,17 @@ public Tuple CreateGroup() ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); }; BucketGroups.Insert(0, newBucketGroupViewModelItem); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) + /// + /// Starts deletion process in the passed and updates positions of + /// all other accordingly + /// + /// Triggers + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteGroup(BucketGroupViewModelItem bucketGroup) { var index = BucketGroups.IndexOf(bucketGroup) + 1; var bucketGroupsToMove = BucketGroups.ToList().GetRange(index, BucketGroups.Count - index); @@ -202,9 +246,9 @@ public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) { try { - var result = bucketGroup.DeleteGroup(); - if (!result.Item1) throw new Exception(result.Item2); - + if (bucketGroup.Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); + dbContext.DeleteBucketGroup(bucketGroup.BucketGroup); + var dbBucketGroups = new List(); foreach (var bucketGroupViewModelItem in bucketGroupsToMove) { @@ -213,21 +257,27 @@ public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) } dbContext.UpdateBucketGroups(dbBucketGroups); + transaction.Commit(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } } - - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } - public Tuple DistributeBudget() + /// + /// Put money into all Buckets according to their Want. Saves the results to the database. + /// + /// Doesn't consider any available Budget figures. + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult DistributeBudget() { using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -244,25 +294,29 @@ public Tuple DistributeBudget() { if (bucket.Want == 0) continue; bucket.InOut = bucket.Want; - var (success, errorMessage) = bucket.HandleInOutInput("Enter"); - if (!success) throw new Exception(errorMessage); + var result = bucket.HandleInOutInput("Enter"); + if (!result.IsSuccessful) throw new Exception(result.Message); } + transaction.Commit(); + //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Error during Budget distribution: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during Budget distribution: {e.Message}"); } - } } - //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } - public Tuple UpdateBalanceFigures() + /// + /// Re-calculates figures of the ViewModel like Budget and Balances + /// + /// Object which contains information and results of this method + public ViewModelOperationResult UpdateBalanceFigures() { try { @@ -313,12 +367,16 @@ public Tuple UpdateBalanceFigures() } catch (Exception e) { - return new Tuple(false, $"Error during Balance recalculation: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during Balance recalculation: {e.Message}"); } - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } + /// + /// Helper methode to set Collapse status for all + /// + /// New collapse status public void ChangeBucketGroupCollapse(bool collapse = true) { _defaultCollapseState = collapse; @@ -328,17 +386,30 @@ public void ChangeBucketGroupCollapse(bool collapse = true) } } - public Tuple SaveChanges(BucketViewModelItem bucket) + /// + /// Helper method to start Save process for the passed + /// + /// Triggers also update of ViewModel figures + /// instance with modifications + /// Object which contains information and results of this method + public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) { - var result = bucket.SaveChanges(); - if (!result.Item1) return result; + var result = bucket.CreateOrUpdateBucket(); + if (!result.IsSuccessful) return result; return UpdateBalanceFigures(); } - public Tuple CloseBucket(BucketViewModelItem bucket) + /// + /// Helper method to start Deletion process for the passed + /// + /// Triggers also update of ViewModel figures + /// Triggers + /// instance with containing to be closed + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket(BucketViewModelItem bucket) { var result = bucket.CloseBucket(); - if (!result.Item1) return result; + if (!result.IsSuccessful) return result; return UpdateBalanceFigures(); } } diff --git a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs index bc165af..74886a7 100644 --- a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; using TinyCsvParser; using TinyCsvParser.Mapping; using TinyCsvParser.Tokenizer.RFC4180; @@ -21,6 +22,15 @@ public class ImportDataViewModel : ViewModelBase { private class CsvBankTransactionMapping : CsvMapping { + /// + /// Definition on how CSV columns should be mapped to + /// + /// + /// instance and collection of columns will be used to identify columnIndex for + /// CSV mapping + /// + /// Instance required for CSV column name + /// Collection of all CSV columns public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable identifiedColumns) : base() { // TODO Add User Input for CultureInfo for Amount & TransactionDate conversion @@ -36,6 +46,9 @@ public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable + /// Path to the file which should be imported + ///
public string FilePath { get => _filePath; @@ -43,6 +56,9 @@ public string FilePath } private string _fileText; + /// + /// Readonly content of the file + /// public string FileText { get => _fileText; @@ -50,6 +66,9 @@ public string FileText } private Account _selectedAccount; + /// + /// Target for which all imported should be added + /// public Account SelectedAccount { get => _selectedAccount; @@ -57,6 +76,9 @@ public Account SelectedAccount } private ImportProfile _selectedImportProfile; + /// + /// Selected profile with import settings from the database + /// public ImportProfile SelectedImportProfile { get => _selectedImportProfile; @@ -64,72 +86,83 @@ public ImportProfile SelectedImportProfile } private int _totalRecords; + /// + /// Number of records identified in the file + /// public int TotalRecords { get => _totalRecords; - set => Set(ref _totalRecords, value); + private set => Set(ref _totalRecords, value); } private int _recordsWithErrors; + /// + /// Number of records where import will fail or has failed + /// public int RecordsWithErrors { get => _recordsWithErrors; - set => Set(ref _recordsWithErrors, value); + private set => Set(ref _recordsWithErrors, value); } private int _validRecords; + /// + /// Number of records where import will be or was successful + /// public int ValidRecords { get => _validRecords; - set => Set(ref _validRecords, value); - } - - private bool _isModificationEnabled; - public bool IsModificationEnabled - { - get => _isModificationEnabled; - set => Set(ref _isModificationEnabled, value); - } - - private bool _isColumnMappingSettingVisible; - public bool IsColumnMappingSettingVisible - { - get => _isColumnMappingSettingVisible; - set => Set(ref _isColumnMappingSettingVisible, value); + private set => Set(ref _validRecords, value); } private ObservableCollection _availableImportProfiles; + /// + /// Available in the database + /// public ObservableCollection AvailableImportProfiles { get => _availableImportProfiles; - set => Set(ref _availableImportProfiles, value); + private set => Set(ref _availableImportProfiles, value); } private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all available in the database + /// public ObservableCollection AvailableAccounts { get => _availableAccounts; - set => Set(ref _availableAccounts, value); + private set => Set(ref _availableAccounts, value); } private ObservableCollection _identifiedColumns; + /// + /// Collection of columns that have been identified in the CSV file + /// public ObservableCollection IdentifiedColumns { get => _identifiedColumns; - set => Set(ref _identifiedColumns, value); + private set => Set(ref _identifiedColumns, value); } private ObservableCollection> _parsedRecords; + /// + /// Results of + /// public ObservableCollection> ParsedRecords { get => _parsedRecords; - set => Set(ref _parsedRecords, value); + private set => Set(ref _parsedRecords, value); } private bool _isProfileValid; private string[] _fileLines; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public ImportDataViewModel(DbContextOptions dbOptions) { AvailableImportProfiles = new ObservableCollection(); @@ -141,7 +174,11 @@ public ImportDataViewModel(DbContextOptions dbOptions) _dbOptions = dbOptions; } - public Tuple LoadData() + /// + /// Initialize ViewModel and load data from database + /// + /// + public ViewModelOperationResult LoadData() { try { @@ -153,34 +190,58 @@ public Tuple LoadData() AvailableAccounts.Add(account); } } + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple HandleOpenFile(string[] dialogResults) + /// + /// Open a file based on and read its content + /// + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile() { try { - if (!dialogResults.Any()) return new Tuple(true, string.Empty); - FilePath = dialogResults[0]; FileText = File.ReadAllText(FilePath, Encoding.GetEncoding(1252)); _fileLines = File.ReadAllLines(FilePath, Encoding.GetEncoding(1252)); - IsModificationEnabled = true; + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); - } - public async Task> HandleOpenFileAsync(Stream stream) + /// + /// Open a file based on results of an OpenFileDialog and read its content + /// + /// OpenFileDialog results + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile(string[] dialogResults) + { + try + { + if (!dialogResults.Any()) return new ViewModelOperationResult(true); + FilePath = dialogResults[0]; + return HandleOpenFile(); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + } + + /// + /// Open a file based on a and read its content + /// + /// Stream to the file + /// Object which contains information and results of this method + public async Task HandleOpenFileAsync(Stream stream) { try { @@ -200,22 +261,23 @@ public async Task> HandleOpenFileAsync(Stream stream) FileText = stringBuilder.ToString(); _fileLines = newLines.ToArray(); - IsModificationEnabled = true; + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to open file: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to open file: {e.Message}"); } - return new Tuple(true, string.Empty); - } - public Tuple LoadProfile() + /// + /// Loads all settings based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadProfile() { try { ResetLoadedProfileData(); - IsColumnMappingSettingVisible = true; // Set target Account if (AvailableAccounts.Any(i => i.AccountId == SelectedImportProfile.AccountId)) @@ -223,22 +285,26 @@ public Tuple LoadProfile() SelectedAccount = AvailableAccounts.First(i => i.AccountId == SelectedImportProfile.AccountId); } - var (success, errorMessage) = LoadHeaders(); - if (!success) throw new Exception(errorMessage); + var result = LoadHeaders(); + if (!result.IsSuccessful) throw new Exception(result.Message); ValidateData(); - _isProfileValid = true; + + return new ViewModelOperationResult(true); } catch (Exception e) { _isProfileValid = false; - return new Tuple(false, $"Unable to load Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to load Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple LoadHeaders() + /// + /// Reads column headers from the loaded file + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadHeaders() { try { @@ -251,25 +317,36 @@ public Tuple LoadHeaders() if (column != string.Empty) IdentifiedColumns.Add(column.Trim(SelectedImportProfile.TextQualifier)); // Exclude TextQualifier } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to load Headers: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to load Headers: {e.Message}"); } - return new Tuple(true, string.Empty); } + /// + /// Reset all figures and parsed records + /// private void ResetLoadedProfileData() { SelectedAccount = new Account(); - IsColumnMappingSettingVisible = false; ParsedRecords.Clear(); TotalRecords = 0; RecordsWithErrors = 0; ValidRecords = 0; } - public string ValidateData() + /// + /// Reads the file and parses the content to a set of . + /// Results will be stored in + /// + /// + /// Sets also figures of the ViewModel like or + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ValidateData() { try { @@ -303,7 +380,7 @@ public string ValidateData() ValidRecords = parsedResults.Count(i => i.IsValid); if (ValidRecords > 0) _isProfileValid = true; - return string.Empty; + return new ViewModelOperationResult(true); } catch (Exception e) { @@ -311,16 +388,21 @@ public string ValidateData() RecordsWithErrors = 0; ValidRecords = 0; ParsedRecords.Clear(); - return e.Message; + return new ViewModelOperationResult(false, e.Message); } } - public Tuple ImportData() + /// + /// Uses data from to store it in the database + /// + /// + /// This method will call + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ImportData() { - if (!_isProfileValid) - return new Tuple(false, "Unable to Import Data as current settings are invalid."); - - var importedCount = 0; + if (!_isProfileValid) return new ViewModelOperationResult(false, "Unable to Import Data as current settings are invalid."); + ValidateData(); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -328,6 +410,7 @@ public Tuple ImportData() { try { + var importedCount = 0; var newRecords = new List(); foreach (var parsedRecord in ParsedRecords.Where(i => i.IsValid)) { @@ -336,19 +419,22 @@ public Tuple ImportData() newRecords.Add(newRecord); } importedCount = dbContext.CreateBankTransactions(newRecords); + transaction.Commit(); + return new ViewModelOperationResult(true, $"Successfully imported {importedCount} records."); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Unable to Import Data. Error message: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to Import Data. Error message: {e.Message}"); } } } - - return new Tuple(true, $"Successfully imported {importedCount} records."); } + /// + /// Helper method to load from the database + /// private void LoadAvailableProfiles() { AvailableImportProfiles.Clear(); @@ -361,7 +447,11 @@ private void LoadAvailableProfiles() } } - public Tuple CreateProfile() + /// + /// Creates a new in the database based on data + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateProfile() { try { @@ -374,16 +464,21 @@ public Tuple CreateProfile() throw new Exception("Profile could not be created in database."); LoadAvailableProfiles(); SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } + } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to create Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to create Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple SaveProfile() + /// + /// Updates data of the current in the database + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveProfile() { try { @@ -395,16 +490,21 @@ public Tuple SaveProfile() LoadAvailableProfiles(); SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } + } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to save Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to save Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple DeleteProfile() + /// + /// Deletes the in the database based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteProfile() { try { @@ -414,12 +514,13 @@ public Tuple DeleteProfile() } ResetLoadedProfileData(); LoadAvailableProfiles(); + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to delete Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to delete Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs index 2d54923..a22de03 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; @@ -11,6 +12,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class AccountViewModelItem : ViewModelBase { private Account _account; + /// + /// Reference to model object in the database + /// public Account Account { get => _account; @@ -18,6 +22,9 @@ public Account Account } private decimal _balance; + /// + /// Total account balance + /// public decimal Balance { get => _balance; @@ -25,6 +32,9 @@ public decimal Balance } private decimal _in; + /// + /// Total income of the account + /// public decimal In { get => _in; @@ -32,61 +42,74 @@ public decimal In } private decimal _out; + /// + /// Total expenses of the account + /// public decimal Out { get => _out; set => Set(ref _out, value); } - private bool _inModification; - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } - + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public AccountViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// Account instance public AccountViewModelItem(DbContextOptions dbOptions, Account account) : this(dbOptions) { _account = account; } - public Tuple CreateUpdateAccount() + /// + /// Creates or updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateAccount() { using (var dbContext = new DatabaseContext(_dbOptions)) { var result = Account.AccountId == 0 ? dbContext.CreateAccount(Account) : dbContext.UpdateAccount(Account); - if (result == 0) - { - return new Tuple(false, "Unable to save changes to database"); - } + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } } - public Tuple CloseAccount() + /// + /// Sets Inactive flag for a record in the database based on object. + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseAccount() { - if (Balance != 0) return new Tuple(false, "Balance must be 0 to close an Account"); + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close an Account"); Account.IsActive = 0; using (var dbContext = new DatabaseContext(_dbOptions)) { var result = dbContext.UpdateAccount(Account); - if (result == 0) - { - return new Tuple(false, "Unable to save changes to database"); - } + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs index 9077e63..d09db17 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -13,6 +14,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class BucketGroupViewModelItem : ViewModelBase { private BucketGroup _bucketGroup; + /// + /// Reference to model object in the database + /// public BucketGroup BucketGroup { get => _bucketGroup; @@ -20,6 +24,9 @@ public BucketGroup BucketGroup } private decimal _totalBalance; + /// + /// Balance of all Buckets assigned to the BucketGroup + /// public decimal TotalBalance { get => _totalBalance; @@ -27,6 +34,9 @@ public decimal TotalBalance } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -34,6 +44,9 @@ public bool IsHovered } private bool _isCollapsed; + /// + /// Helper property to check if the list of assigned Buckets is collapsed + /// public bool IsCollapsed { get => _isCollapsed; @@ -41,6 +54,9 @@ public bool IsCollapsed } private ObservableCollection _buckets; + /// + /// Collection of Buckets assigned to this BucketGroup + /// public ObservableCollection Buckets { get => _buckets; @@ -48,18 +64,29 @@ public ObservableCollection Buckets } private bool _inModification; + /// + /// Helper property to check if the BucketGroup is currently modified + /// public bool InModification { get => _inModification; set => Set(ref _inModification, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; - internal DateTime CurrentMonth; + private readonly DateTime _currentMonth; private readonly DbContextOptions _dbOptions; private BucketGroup _oldBucketGroup; + /// + /// Basic constructor + /// + /// Options to connect to a database public BucketGroupViewModelItem(DbContextOptions dbOptions) { Buckets = new ObservableCollection(); @@ -67,12 +94,21 @@ public BucketGroupViewModelItem(DbContextOptions dbOptions) _dbOptions = dbOptions; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used public BucketGroupViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime currentMonth) : this(dbOptions) { BucketGroup = bucketGroup; - CurrentMonth = currentMonth; + _currentMonth = currentMonth; } + /// + /// Helper method to start modification process and creating a backup of current values + /// public void StartModification() { _oldBucketGroup = new BucketGroup() @@ -84,6 +120,9 @@ public void StartModification() InModification = true; } + /// + /// Stops modification and restores previous values + /// public void CancelModification() { BucketGroup = _oldBucketGroup; @@ -91,7 +130,12 @@ public void CancelModification() _oldBucketGroup = null; } - public Tuple SaveModification() + /// + /// Updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult SaveModification() { try { @@ -99,38 +143,27 @@ public Tuple SaveModification() { dbContext.UpdateBucketGroup(BucketGroup); } - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); InModification = false; _oldBucketGroup = null; + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { - return new Tuple(false, $"Unable to write changes to database: {e.Message}"); - } - return new Tuple(true, string.Empty); - } - - public Tuple DeleteGroup() - { - try - { - if (Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); - - using (var dbContext = new DatabaseContext(_dbOptions)) - { - dbContext.DeleteBucketGroup(BucketGroup); - } - } - catch (Exception e) - { - return new Tuple(false, $"Unable to delete Bucket Group: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to write changes to database: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple MoveGroup(int positions) + /// + /// Moves the position of the BucketGroup according to the passed value. Updates positions for all other + /// BucketGroups accordingly + /// + /// Number of positions that BucketGroup needs to be moved + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult MoveGroup(int positions) { - if (positions == 0) return new Tuple(true, string.Empty); + if (positions == 0) return new ViewModelOperationResult(true); using (var dbContext = new DatabaseContext(_dbOptions)) { using (var transaction = dbContext.Database.BeginTransaction()) @@ -141,7 +174,7 @@ public Tuple MoveGroup(int positions) var targetPosition = BucketGroup.Position + positions; if (targetPosition < 1) targetPosition = 1; if (targetPosition > bucketGroupCount) targetPosition = bucketGroupCount; - if (targetPosition == BucketGroup.Position) return new Tuple(true, string.Empty); // Group is already at the end or top. No further action + if (targetPosition == BucketGroup.Position) return new ViewModelOperationResult(true); // Group is already at the end or top. No further action // Move Group in an interim List var existingBucketGroups = new ObservableCollection(); foreach (var bucketGroup in dbContext.BucketGroup.OrderBy(i => i.Position)) @@ -161,21 +194,20 @@ public Tuple MoveGroup(int positions) transaction.Commit(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Unable to move Bucket Group: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to move Bucket Group: {e.Message}"); } } } - - return new Tuple(true, string.Empty); } public BucketViewModelItem CreateBucket() { - var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, CurrentMonth); + var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, _currentMonth); // Hand over ViewModel changes newBucket.ViewModelReloadRequired += (sender, args) => ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index 2b0707a..940b06a 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -9,6 +9,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -16,6 +17,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class BucketViewModelItem : ViewModelBase { private Bucket _bucket; + /// + /// Reference to model object in the database + /// public Bucket Bucket { get => _bucket; @@ -23,6 +27,9 @@ public Bucket Bucket } private BucketVersion _bucketVersion; + /// + /// Reference to model object in the database + /// public BucketVersion BucketVersion { get => _bucketVersion; @@ -101,7 +108,7 @@ public int Progress private bool _isProgressBarVisible; /// - /// Sets the visibility of the ProgressBar if 3 or 4 + /// Helper property to set the visibility of the ProgressBar if 3 or 4 /// public bool IsProgressbarVisible { @@ -110,6 +117,9 @@ public bool IsProgressbarVisible } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -117,6 +127,9 @@ public bool IsHovered } private bool _inModification; + /// + /// Helper property to check if the Bucket is currently modified + /// public bool InModification { get => _inModification; @@ -124,6 +137,9 @@ public bool InModification } private ObservableCollection _availableBucketTypes; + /// + /// Helper collection to list BucketTypes explanations + /// public ObservableCollection AvailableBucketTypes { get => _availableBucketTypes; @@ -131,6 +147,9 @@ public ObservableCollection AvailableBucketTypes } private ObservableCollection _availableColors; + /// + /// Helper collection to list available System colors + /// public ObservableCollection AvailableColors { get => _availableColors; @@ -138,18 +157,29 @@ public ObservableCollection AvailableColors } private ObservableCollection _availableBucketGroups; + /// + /// Helper collection to list available where this Bucket can be assigned to + /// public ObservableCollection AvailableBucketGroups { get => _availableBucketGroups; set => Set(ref _availableBucketGroups, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly bool _isNewlyCreatedBucket; private readonly DateTime _currentYearMonth; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public BucketViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; @@ -183,6 +213,12 @@ void GetKnownColors() } } + /// + /// Initialize ViewModel based on a specific YearMonth + /// + /// Creates an initial + /// Options to connect to a database + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) : this(dbOptions) { _currentYearMonth = new DateTime(yearMonth.Year, yearMonth.Month, 1); @@ -195,6 +231,14 @@ public BucketViewModelItem(DbContextOptions dbOptions, DateTime }; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Creates an initial in active modification mode + /// Creates an initial + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime yearMonth) : this(dbOptions, yearMonth) { _isNewlyCreatedBucket = true; @@ -210,17 +254,34 @@ public BucketViewModelItem(DbContextOptions dbOptions, BucketGr }; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Runs to get latest + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) : this(dbOptions, yearMonth) { Bucket = bucket; CalculateValues(); } + /// + /// Creates and returns a new ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used + /// New ViewModel instance public static async Task CreateAsync(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) { return await Task.Run(() => new BucketViewModelItem(dbOptions, bucket, yearMonth)); } + /// + /// Identifies latest based on and calculates all figures + /// private void CalculateValues() { Balance = 0; @@ -375,15 +436,26 @@ decimal CalculateWant(DateTime targetDate) #endregion } + /// + /// Activates modification mode + /// public void EditBucket() { InModification = true; } - public Tuple CloseBucket() + /// + /// Updates a record in the database based on object to set it as inactive. In case there + /// are no nor assigned to it, it will be deleted + /// completely from the database (including ) + /// + /// Bucket will be set to inactive for the next month + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket() { - if (Bucket.IsInactive) return new Tuple(false, "Bucket has been already set to inactive"); - if (Balance != 0) return new Tuple(false, "Balance must be 0 to close a Bucket"); + if (Bucket.IsInactive) return new ViewModelOperationResult(false, "Bucket has been already set to inactive"); + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close a Bucket"); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -427,145 +499,180 @@ public Tuple CloseBucket() catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Error during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } } } ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } - public Tuple SaveChanges() + /// + /// Creates or updates a record in the database based on object + /// + /// Creates also a new record in the database + /// + /// Recalculates figures after database operations in case has not been triggered + /// + /// Can trigger + /// Object which contains information and results of this method + public ViewModelOperationResult CreateOrUpdateBucket() { - var forceViewModelReload = false; + var result = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); + if (!result.IsSuccessful || result.ViewModelReloadInvoked) return result; + InModification = false; + CalculateValues(); + return new ViewModelOperationResult(true); + } - if (_isNewlyCreatedBucket) + /// + /// Creates a new record in the database based on object + /// + /// Creates also a new record in the database + /// Triggers + /// Object which contains information and results of this method + private ViewModelOperationResult CreateBucket() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - // Create new Bucket - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try - { - if (dbContext.CreateBucket(Bucket) == 0) - throw new Exception("Unable to create new Bucket."); - - var newBucketVersion = BucketVersion; - newBucketVersion.BucketId = Bucket.BucketId; - newBucketVersion.Version = 1; - newBucketVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(newBucketVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {newBucketVersion.BucketId}"); + if (dbContext.CreateBucket(Bucket) == 0) + throw new Exception("Unable to create new Bucket."); + + var newBucketVersion = BucketVersion; + newBucketVersion.BucketId = Bucket.BucketId; + newBucketVersion.Version = 1; + newBucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(newBucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {newBucketVersion.BucketId}"); - transaction.Commit(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - } - catch (Exception e) - { - transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, $"Error during database update: {e.Message}"); - } + transaction.Commit(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } - } - } - else - { - // Check on Bucket changes and update database - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); - if (dbBucket.Name != Bucket.Name || - dbBucket.ColorCode != Bucket.ColorCode || - dbBucket.BucketGroupId != Bucket.BucketGroupId) + catch (Exception e) { - // BucketGroup update requires special handling as ViewModel needs to trigger reload - // to force re-rendering of Blazor Page - if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; - - if (dbContext.UpdateBucket(Bucket) == 0) - return new Tuple(false, - $"Error during database update: Unable to update Bucket.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {Bucket.BucketId}"); + transaction.Rollback(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); } } + } + } - // Check on BucketVersion changes and create new BucketVersion - using (var dbContext = new DatabaseContext(_dbOptions)) + /// + /// Updates a record in the database based on object + /// + /// Creates also a new record in the database + /// Can trigger + /// Object which contains information and results of this method + private ViewModelOperationResult UpdateBucket() + { + var forceViewModelReload = false; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + using (var transaction = dbContext.Database.BeginTransaction()) { - var dbBucketVersion = - dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); - if (dbBucketVersion.BucketType != BucketVersion.BucketType || - dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || - dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || - dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || - dbBucketVersion.Notes != BucketVersion.Notes) + try { - using (var transaction = dbContext.Database.BeginTransaction()) + // Check on Bucket changes and update database + var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); + if (dbBucket.Name != Bucket.Name || + dbBucket.ColorCode != Bucket.ColorCode || + dbBucket.BucketGroupId != Bucket.BucketGroupId) { - try + // BucketGroup update requires special handling as ViewModel needs to trigger reload + // to force re-rendering of Blazor Page + if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; + + if (dbContext.UpdateBucket(Bucket) == 0) + throw new Exception($"Error during database update: Unable to update Bucket.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {Bucket.BucketId}"); + } + + // Check on BucketVersion changes and create new BucketVersion + var dbBucketVersion = + dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); + if (dbBucketVersion.BucketType != BucketVersion.BucketType || + dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || + dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || + dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || + dbBucketVersion.Notes != BucketVersion.Notes) + { + if (dbContext.BucketVersion.Any(i => + i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) + throw new Exception("Cannot create new Version as already a newer Version exists"); + + if (BucketVersion.ValidFrom == _currentYearMonth) { - if (dbContext.BucketVersion.Any(i => i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) - throw new Exception("Cannot create new Version as already a newer Version exists"); - - var modifiedVersion = BucketVersion; - if (BucketVersion.ValidFrom == _currentYearMonth) - { - // Bucket Version modified in the same month, - // so just update the version instead of creating a new version - if (dbContext.UpdateBucketVersion(modifiedVersion) == 0) - throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket Version ID: {modifiedVersion.BucketVersionId}" + - $"Bucket ID: {modifiedVersion.BucketId}" + - $"Bucket Version: {modifiedVersion.Version}" + - $"Bucket Version Start Date: {modifiedVersion.ValidFrom.ToShortDateString()}"); - } - else - { - modifiedVersion.Version++; - modifiedVersion.BucketVersionId = 0; - modifiedVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(modifiedVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {modifiedVersion.BucketId}" + - $"Bucket Version: {modifiedVersion.Version}" + - $"Bucket Version Start Date: {modifiedVersion.ValidFrom.ToShortDateString()}"); - } - - transaction.Commit(); - //ViewModelReloadRequired?.Invoke(this); + // Bucket Version modified in the same month, + // so just update the version instead of creating a new version + if (dbContext.UpdateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket Version ID: {BucketVersion.BucketVersionId}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } - catch (Exception e) + else { - transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, $"Error during database update: {e.Message}"); + BucketVersion.Version++; + BucketVersion.BucketVersionId = 0; + BucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } } + transaction.Commit(); + if (forceViewModelReload) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } - } + catch (Exception e) + { + transaction.Rollback(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); + } + } } - InModification = false; - CalculateValues(); - if (forceViewModelReload) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all modifications + /// public void CancelChanges() { InModification = false; ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); // force Re-load to get old values back } - public Tuple HandleInOutInput(string key) + /// + /// Helper method to create a new record in the database based on User input + /// + /// Creation starts once Enter key is pressed + /// Recalculates figures after database operations + /// Pressed key + /// Object which contains information and results of this method + public ViewModelOperationResult HandleInOutInput(string key) { - if (key != "Enter") return new Tuple(true, string.Empty); + if (key != "Enter") return new ViewModelOperationResult(true); try { using (var dbContext = new DatabaseContext(_dbOptions)) @@ -580,12 +687,12 @@ public Tuple HandleInOutInput(string key) } //ViewModelReloadRequired?.Invoke(this); CalculateValues(); + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } - return new Tuple(true, string.Empty); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs index cb1345b..acac5a6 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -10,6 +10,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class MappingRuleViewModelItem : ViewModelBase { private MappingRule _mappingRule; + /// + /// Reference to model object in the database + /// public MappingRule MappingRule { get => _mappingRule; @@ -17,6 +20,9 @@ public MappingRule MappingRule } private string _ruleOutput; + /// + /// Helper property to generate a readable output for + /// public string RuleOutput { get => _ruleOutput; @@ -25,17 +31,29 @@ public string RuleOutput private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public MappingRuleViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// MappingRule instance public MappingRuleViewModelItem(DbContextOptions dbOptions, MappingRule mappingRule) : this(dbOptions) { MappingRule = mappingRule; GenerateRuleOutput(); } + /// + /// Translates object into a readable format + /// public void GenerateRuleOutput() { RuleOutput = MappingRule == null ? string.Empty : diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs index f6cdaa5..b167ab0 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs @@ -5,9 +5,15 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { + /// + /// Helper class for Reports showing monthly Bucket expenses + /// public class MonthlyBucketExpensesReportViewModelItem : ViewModelBase { private string _bucketName; + /// + /// Name of the Bucket + /// public string BucketName { get => _bucketName; @@ -15,12 +21,18 @@ public string BucketName } private ObservableCollection> _monthlyResults; + /// + /// Collection of the results for the report + /// public ObservableCollection> MonthlyResults { get => _monthlyResults; set => Set(ref _monthlyResults, value); } + /// + /// Basic constructor + /// public MonthlyBucketExpensesReportViewModelItem() { MonthlyResults = new ObservableCollection>(); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs index d7e5f2c..d518cec 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; @@ -11,11 +11,14 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { /// - /// ViewModel to handle the multi-assignment of Buckets to one + /// Helper ViewModel to handle the multi-assignment of Buckets to one /// public class PartialBucketViewModelItem : ViewModelBase { private Bucket _selectedBucket; + /// + /// Affected Bucket + /// public Bucket SelectedBucket { get => _selectedBucket; @@ -23,6 +26,9 @@ public Bucket SelectedBucket } private string _selectedBucketOutput; + /// + /// Helper property to generate an output for the Bucket including the assigned amount + /// public string SelectedBucketOutput { get => _selectedBucketOutput; @@ -30,6 +36,9 @@ public string SelectedBucketOutput } private decimal _amount; + /// + /// Money that will be assigned to this Bucket + /// public decimal Amount { get => _amount; @@ -41,25 +50,38 @@ public decimal Amount } private ObservableCollection _availableBuckets; + /// + /// Helper collection with all available Buckets + /// public ObservableCollection AvailableBuckets { get => _availableBuckets; set => Set(ref _availableBuckets, value); } + /// + /// EventHandler which should be invoked once amount assigned to this Bucket has been changed. Can be used + /// to start further consistency checks and other calculations based on this change + /// public event EventHandler AmountChanged; + /// + /// EventHandler which should be invoked in case this instance should start its deletion process. Can be used + /// in case the way how this instance will be deleted is handled outside of this class + /// public event EventHandler DeleteAssignmentRequest; - public PartialBucketViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) + /// + /// Basic constructor + /// + /// Options to connect to a database + /// Current YearMonth + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) { - AvailableBuckets = new ObservableCollection(); - // Add empty Bucket for empty pre-selection - AvailableBuckets.Add(new Bucket + AvailableBuckets = new ObservableCollection { - BucketId = 0, - BucketGroupId = 0, - Name = "No Selection" - }); + new Bucket {BucketId = 0, BucketGroupId = 0, Name = "No Selection"} + }; + // Add empty Bucket for empty pre-selection using (var dbContext = new DatabaseContext(dbOptions)) { foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) @@ -69,9 +91,9 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y var query = dbContext.Bucket .Where(i => i.BucketId > 2 && - i.ValidFrom <= yearMonthViewModel.CurrentMonth && + i.ValidFrom <= yearMonth && (i.IsInactive == false || - (i.IsInactive && i.IsInactiveFrom > yearMonthViewModel.CurrentMonth))) + (i.IsInactive && i.IsInactiveFrom > yearMonth))) .OrderBy(i => i.Name); foreach (var availableBucket in query.ToList()) @@ -82,7 +104,14 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y SelectedBucket = AvailableBuckets.First(); } - public PartialBucketViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, Bucket bucket, decimal amount) : this(dbOptions, yearMonthViewModel) + /// + /// Initialize ViewModel based on an existing object and the final amount to be assigned + /// + /// Options to connect to a database + /// Current YearMonth + /// Bucket instance + /// Amount to be assigned to this Bucket + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth, Bucket bucket, decimal amount) : this(dbOptions, yearMonth) { Amount = amount; foreach (var availableBucket in AvailableBuckets) @@ -96,6 +125,9 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y if (SelectedBucket == null) SelectedBucket = AvailableBuckets.First(); } + /// + /// Triggers + /// public void DeleteBucket() { DeleteAssignmentRequest?.Invoke(this, new DeleteAssignmentRequestArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs index 30f0d55..a920d93 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs @@ -5,6 +5,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -12,6 +13,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class RuleSetViewModelItem : ViewModelBase { private BucketRuleSet _ruleSet; + /// + /// Reference to model object in the database + /// public BucketRuleSet RuleSet { get => _ruleSet; @@ -19,14 +23,19 @@ public BucketRuleSet RuleSet } private Bucket _targetBucket; + /// + /// Bucket to which this RuleSet applies + /// public Bucket TargetBucket { get => _targetBucket; set => Set(ref _targetBucket, value); } - private bool _inModification; + /// + /// Helper property to check if the RuleSet is currently modified + /// public bool InModification { get => _inModification; @@ -34,6 +43,9 @@ public bool InModification } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -41,6 +53,9 @@ public bool IsHovered } private ObservableCollection _mappingRules; + /// + /// Collection of MappingRules assigned to this RuleSet + /// public ObservableCollection MappingRules { get => _mappingRules; @@ -48,6 +63,9 @@ public ObservableCollection MappingRules } private ObservableCollection _availableBuckets; + /// + /// Helper collection to list all existing Buckets + /// public ObservableCollection AvailableBuckets { get => _availableBuckets; @@ -57,16 +75,16 @@ public ObservableCollection AvailableBuckets private readonly DbContextOptions _dbOptions; private RuleSetViewModelItem _oldRuleSetViewModelItem; - public RuleSetViewModelItem() + /// + /// Basic constructor + /// + /// Options to connect to a database + public RuleSetViewModelItem(DbContextOptions dbOptions) { MappingRules = new ObservableCollection(); AvailableBuckets = new ObservableCollection(); RuleSet = new BucketRuleSet(); TargetBucket = new Bucket(); - } - - public RuleSetViewModelItem(DbContextOptions dbOptions) : this() - { _dbOptions = dbOptions; AvailableBuckets.Add(new Bucket { @@ -92,6 +110,11 @@ public RuleSetViewModelItem(DbContextOptions dbOptions) : this( } } + /// + /// Initialize ViewModel based on an existing + /// + /// Options to connect to a database + /// RuleSet instance public RuleSetViewModelItem(DbContextOptions dbOptions, BucketRuleSet bucketRuleSet) : this(dbOptions) { @@ -113,12 +136,18 @@ public RuleSetViewModelItem(DbContextOptions dbOptions, BucketR } } + /// + /// Helper method to start modification process + /// public void StartModification() { _oldRuleSetViewModelItem = new RuleSetViewModelItem(_dbOptions, RuleSet); InModification = true; } + /// + /// Stops modification process and restores old values + /// public void CancelModification() { RuleSet = _oldRuleSetViewModelItem.RuleSet; @@ -127,14 +156,20 @@ public void CancelModification() _oldRuleSetViewModelItem = null; } + /// + /// Creates an initial and adds it to the + /// public void AddEmptyMappingRule() { MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule())); } - public Tuple CreateUpdateRuleSetItem() + /// + /// Creates or updates records in the database based on and objects + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateRuleSetItem() { - var result = new Tuple(true, string.Empty); using (var dbContext = new DatabaseContext(_dbOptions)) { using (var dbTransaction = dbContext.Database.BeginTransaction()) @@ -167,23 +202,28 @@ public Tuple CreateUpdateRuleSetItem() dbContext.CreateMappingRules(MappingRules.Select(i => i.MappingRule).ToList()); dbTransaction.Commit(); + _oldRuleSetViewModelItem = null; + InModification = false; + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - result = new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } - _oldRuleSetViewModelItem = null; - InModification = false; - - return result; } + /// + /// Deletes passed MappingRule from the collection + /// + /// MappingRule that needs to be removed public void DeleteMappingRule(MappingRuleViewModelItem mappingRule) { + //Note: Doesn't require any database updates as this will be done during CreateUpdateRuleSetItem MappingRules.Remove(mappingRule); if (MappingRules.Count == 0) AddEmptyMappingRule(); } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index 19f7106..5816f83 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; @@ -8,15 +8,16 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { - /// - /// ViewModel for each Transaction Item - /// public class TransactionViewModelItem : ViewModelBase { private BankTransaction _transaction; + /// + /// Reference to model object in the database + /// public BankTransaction Transaction { get => _transaction; @@ -24,6 +25,9 @@ public BankTransaction Transaction } private Account _selectedAccount; + /// + /// Account where the Transaction is assigned to + /// public Account SelectedAccount { get => _selectedAccount; @@ -31,6 +35,9 @@ public Account SelectedAccount } private bool _inModification; + /// + /// Helper property to check if the Transaction is currently modified + /// public bool InModification { get => _inModification; @@ -38,6 +45,9 @@ public bool InModification } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -45,6 +55,9 @@ public bool IsHovered } private ObservableCollection _buckets; + /// + /// Collection of Buckets which are assigned to this Transaction + /// public ObservableCollection Buckets { get => _buckets; @@ -52,18 +65,28 @@ public ObservableCollection Buckets } private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all existing Account + /// public ObservableCollection AvailableAccounts { get => _availableAccounts; set => Set(ref _availableAccounts, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; private TransactionViewModelItem _oldTransactionViewModelItem; + /// + /// Basic constructor + /// public TransactionViewModelItem() { Transaction = new BankTransaction(); @@ -71,6 +94,11 @@ public TransactionViewModelItem() AvailableAccounts = new ObservableCollection(); } + /// + /// Basic constructor + /// + /// Options to connect to a database + /// YearMonth ViewModel instance public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) : this() { _dbOptions = dbOptions; @@ -96,6 +124,13 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea SelectedAccount = AvailableAccounts.First(); } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// Include assigned Buckets public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) { if (withBuckets) @@ -114,7 +149,7 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea using (var bucketDbContext = new DatabaseContext(_dbOptions)) { var newItem = new PartialBucketViewModelItem(_dbOptions, - _yearMonthViewModel, + _yearMonthViewModel.CurrentMonth, bucketDbContext.Bucket.FirstOrDefault(i => i.BucketId == assignedBucket.BucketId), assignedBucket.Amount); newItem.SelectedBucketOutput = @@ -126,7 +161,7 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea else { // Most likely an imported Transaction where Bucket assignment still needs to be done - var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, new Bucket() { BucketId = 0 }, transaction.Amount); + var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket() { BucketId = 0 }, transaction.Amount); Buckets.Add(newItem); } } @@ -165,6 +200,10 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea } + /// + /// Initialize ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed public TransactionViewModelItem(BucketMovement bucketMovement) : this() { // Simulate a BankTransaction based on BucketMovement @@ -187,21 +226,47 @@ public TransactionViewModelItem(BucketMovement bucketMovement) : this() }; } + /// + /// Initialize and return a new ViewModel based on an existing object including + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance public static async Task CreateAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction)); } + /// + /// Initialize and return a new ViewModel based on an existing object without + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance public static async Task CreateWithoutBucketsAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction, false)); } + /// + /// Create and return a new ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed + /// New ViewModel instance public static async Task CreateFromBucketMovementAsync(BucketMovement bucketMovement) { return await Task.Run(() => new TransactionViewModelItem(bucketMovement)); } + /// + /// Event that checks amount for all assigned Buckets and creates or removes an "empty item" + /// + /// Object that has triggered the event + /// Event Arguments about changed amount private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs) { // Check if this current event was triggered while updating the amount for the "emptyItem" @@ -257,6 +322,11 @@ private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs } } + /// + /// Event that handles the deletion of teh requested Bucket + /// + /// Object that has triggered the event + /// Event Arguments about deletion request private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequestArgs args) { // Prevent deletion all last remaining BucketAssignment @@ -266,55 +336,28 @@ private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequ } } + /// + /// Creates an "empty item" with the passed amount + /// + /// Amount that will be assigned to the Bucket private void AddEmptyBucketItem(decimal amount) { // All items have a valid Bucket assignment, create a new "empty item" - var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, new Bucket(), amount); + var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket(), amount); emptyItem.AmountChanged += CheckBucketAssignments; emptyItem.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; Buckets.Add(emptyItem); } - private Tuple CreateUpdateTransaction() + /// + /// Creates or updates a record in the database based on object + /// + /// (Re)Creates also records for each assigned Bucket + /// Object which contains information and results of this method + private ViewModelOperationResult CreateOrUpdateTransaction() { - // Consistency and Validity Checks - if (Transaction == null) return new Tuple(false, "Errors in Transaction object."); - if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new Tuple(false, "No Bank account selected."); - //if (string.IsNullOrEmpty(Transaction.TransactionDate)) - // return new Tuple(false, "Transaction date is missing."); - //if (string.IsNullOrEmpty(transaction.Transaction.Memo)) - // return new Tuple(false, "Transaction Memo is missing."); - if (Buckets.Count == 0) - return new Tuple(false, "No Bucket assigned to this Transaction."); - - decimal assignedAmount = 0; - var skipBucketAssignment = false; - foreach (var assignedBucket in Buckets) - { - if (assignedBucket.SelectedBucket == null) - { - return new Tuple(false, "Pending Bucket assignment for this Transaction."); - } - - if (assignedBucket.SelectedBucket.BucketId == 0) - { - if (assignedBucket.SelectedBucket.Name == "No Selection") - { - // Imported Transaction where Bucket assignment is pending - // Allow Transaction Update but Skip DB Updates for Bucket assignment - skipBucketAssignment = true; - } - else - { - return new Tuple(false, "Pending Bucket assignment for this Transaction."); - } - } - assignedAmount += assignedBucket.Amount; - } - - if (assignedAmount != Transaction.Amount) - return new Tuple(false, - "Amount between Bucket assignment and Transaction not consistent."); + var result = PerformConsistencyCheck(out var skipBucketAssignment); + if (!result.IsSuccessful) return result; using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -332,7 +375,6 @@ private Tuple CreateUpdateTransaction() // Delete all previous bucket assignments for transaction var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == transactionId); dbContext.DeleteBudgetedTransactions(budgetedTransactions); } @@ -360,19 +402,66 @@ private Tuple CreateUpdateTransaction() } transaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } + } - return new Tuple(true, string.Empty); + /// + /// Executes several data consistency checks (e.g. Bucket assignment, pending amount etc.) to see if changes + /// can be stored in the database + /// + /// Exclude checks on Bucket assignment + /// Object which contains information and results of this method + private ViewModelOperationResult PerformConsistencyCheck(out bool skipBucketAssignment) + { + decimal assignedAmount = 0; + skipBucketAssignment = false; + + // Consistency and Validity Checks + if (Transaction == null) return new ViewModelOperationResult(false, "Errors in Transaction object."); + if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new ViewModelOperationResult(false, "No Bank account selected."); + if (Buckets.Count == 0) return new ViewModelOperationResult(false, "No Bucket assigned to this Transaction."); + + foreach (var assignedBucket in Buckets) + { + if (assignedBucket.SelectedBucket == null) + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); + + if (assignedBucket.SelectedBucket.BucketId == 0) + { + if (assignedBucket.SelectedBucket.Name == "No Selection") + { + // Imported Transaction where Bucket assignment is pending + // Allow Transaction Update but Skip DB Updates for Bucket assignment + skipBucketAssignment = true; + } + else + { + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); + } + } + + assignedAmount += assignedBucket.Amount; + } + + if (assignedAmount != Transaction.Amount) return new ViewModelOperationResult(false, "Amount between Bucket assignment and Transaction not consistent."); + + return new ViewModelOperationResult(true); } - private Tuple DeleteTransaction() + /// + /// Removes a record in the database based on object + /// + /// Removes also all its assigned Buckets + /// Object which contains information and results of this method + private ViewModelOperationResult DeleteTransaction() { using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -385,19 +474,19 @@ private Tuple DeleteTransaction() // Delete all previous bucket assignments for transaction var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == Transaction.TransactionId); dbContext.DeleteBudgetedTransactions(budgetedTransactions); + transaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } - return new Tuple(true, string.Empty); } public void StartModification() @@ -415,38 +504,37 @@ public void CancelModification() _oldTransactionViewModelItem = null; } - public Tuple CreateItem() + public ViewModelOperationResult CreateItem() { - Transaction.TransactionId = 0; // Triggers CREATE during CreateUpdateTransaction() - return CreateUpdateTransaction(); + Transaction.TransactionId = 0; // Triggers CREATE during CreateOrUpdateTransaction() + return CreateOrUpdateTransaction(); } - public Tuple UpdateItem() + public ViewModelOperationResult UpdateItem() { - if (Transaction.TransactionId < 1) - return new Tuple(false, "Transaction needs to be created first in database"); + if (Transaction.TransactionId < 1) return new ViewModelOperationResult(false, "Transaction needs to be created first in database"); - var (result, message) = CreateUpdateTransaction(); - if (!result) + var result = CreateOrUpdateTransaction(); + if (!result.IsSuccessful) { // Trigger page reload as DB Update was not successfully ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, message); + return new ViewModelOperationResult(false, result.Message, true); } _oldTransactionViewModelItem = null; InModification = false; - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple DeleteItem() + public ViewModelOperationResult DeleteItem() { - var (result, message) = DeleteTransaction(); - if (!result) return new Tuple(false, message); + var result = DeleteTransaction(); + if (!result.IsSuccessful) return result; ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } public void ProposeBucket() @@ -454,7 +542,7 @@ public void ProposeBucket() var proposal = CheckMappingRules(); if (proposal == null) return; Buckets.Clear(); - Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, proposal, Transaction.Amount)); + Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, proposal, Transaction.Amount)); } private Bucket CheckMappingRules() diff --git a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs index a778336..aaced8f 100644 --- a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations.Operations; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -17,11 +17,25 @@ public class ReportViewModel : ViewModelBase { private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public ReportViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Loads a set of balances per month from the database + /// + /// Considers only within a month + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// public async Task>> LoadMonthBalancesAsync(int months = 24) { return await Task.Run(() => @@ -53,6 +67,16 @@ public async Task>> LoadMonthBalancesAsync(int mon }); } + /// + /// Loads a set of income and expenses per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the income + /// Item3: representing the expenses + /// public async Task>> LoadMonthIncomeExpensesAsync(int months = 24) { return await Task.Run(() => @@ -100,6 +124,15 @@ public async Task>> LoadMonthIncomeExpens }); } + /// + /// Loads a set of income and expenses per year from the database + /// + /// Number of years that should be loaded + /// Collection of containing + /// Item1: representing the year + /// Item2: representing the income + /// Item3: representing the expenses + /// public async Task>> LoadYearIncomeExpensesAsync(int years = 5) { return await Task.Run(() => @@ -147,6 +180,16 @@ public async Task>> LoadYearIncomeExpense }); } + /// + /// Loads a set of balances per month from the database showing the progress of the overall bank balance + /// + /// Considers all from the past + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// public async Task>> LoadBankBalancesAsync(int months = 24) { return await Task.Run(() => @@ -171,6 +214,13 @@ public async Task>> LoadBankBalancesAsync(int mont }); } + /// + /// Loads a set of expenses of a per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of ViewModelItems containing information about a and its expenses per month + /// public async Task> LoadMonthExpensesBucketAsync(int month = 12) { return await Task.Run(() => diff --git a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs index 0aeaf8f..bdef627 100644 --- a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -15,6 +16,9 @@ namespace OpenBudgeteer.Core.ViewModels public class RulesViewModel : ViewModelBase { private RuleSetViewModelItem _newRuleSet; + /// + /// Helper property to handle setup of a new + /// public RuleSetViewModelItem NewRuleSet { get => _newRuleSet; @@ -22,28 +26,39 @@ public RuleSetViewModelItem NewRuleSet } private ObservableCollection _ruleSets; + /// + /// Collection of all from the database + /// public ObservableCollection RuleSets { get => _ruleSets; set => Set(ref _ruleSets, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; - public RulesViewModel() - { - RuleSets = new ObservableCollection(); - } - - public RulesViewModel(DbContextOptions dbOptions) : this() + /// + /// Basic constructor + /// + /// Options to connect to a database + public RulesViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; + RuleSets = new ObservableCollection(); ResetNewRuleSet(); } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { return await Task.Run(() => { @@ -57,30 +72,35 @@ public async Task> LoadDataAsync() RuleSets.Add(new RuleSetViewModelItem(_dbOptions, bucketRuleSet)); } } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); - }); } - public Tuple CreateNewRuleSet() + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateNewRuleSet() { NewRuleSet.RuleSet.BucketRuleSetId = 0; - var (result, message) = NewRuleSet.CreateUpdateRuleSetItem(); - if (!result) - { - return new Tuple(false, message); - } + var result = NewRuleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) return result; ResetNewRuleSet(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } + /// + /// Helper method to reset values of + /// public void ResetNewRuleSet() { NewRuleSet = new RuleSetViewModelItem(_dbOptions); @@ -93,43 +113,55 @@ public void ResetNewRuleSet() })); } - public Tuple SaveRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Starts Creation or Update process for the passed + /// + /// Updates collection + /// Instance that needs to be created or updated + /// Object which contains information and results of this method + public ViewModelOperationResult SaveRuleSetItem(RuleSetViewModelItem ruleSet) { var result = ruleSet.CreateUpdateRuleSetItem(); - if (!result.Item1) return result; + if (!result.IsSuccessful) return result; RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); return result; } - public Tuple DeleteRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Starts Deletion process for the passed including all its + /// + /// Updates collection + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteRuleSetItem(RuleSetViewModelItem ruleSet) { - var result = new Tuple(true, string.Empty); - using (var dbContext = new DatabaseContext(_dbOptions)) { using (var dbTransaction = dbContext.Database.BeginTransaction()) { try { - dbContext.DeleteMappingRules(dbContext.MappingRule.Where(i => - i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); + dbContext.DeleteMappingRules(dbContext.MappingRule + .Where(i => i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); dbContext.DeleteBucketRuleSet(ruleSet.RuleSet); dbTransaction.Commit(); RuleSets.Remove(ruleSet); + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } - } - - return result; } + /// + /// Helper method to start Modification process for all + /// public void EditAllRules() { foreach (var ruleSet in RuleSets) @@ -138,7 +170,12 @@ public void EditAllRules() } } - public Tuple SaveAllRules() + /// + /// Starts the Creation or Update process for all + /// + /// Updates collection + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllRules() { using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { @@ -146,21 +183,25 @@ public Tuple SaveAllRules() { foreach (var ruleSet in RuleSets) { - (bool success, string message) = ruleSet.CreateUpdateRuleSetItem(); - if (!success) throw new Exception(message); + var result = ruleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } dbTransaction.Commit(); RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all changes to all + /// public void CancelAllRules() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index 6d040d4..d51e5fd 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -11,6 +11,7 @@ using System.Windows; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels @@ -18,6 +19,9 @@ namespace OpenBudgeteer.Core.ViewModels public class TransactionViewModel : ViewModelBase { private TransactionViewModelItem _newTransaction; + /// + /// Helper property to handle creation of a new + /// public TransactionViewModelItem NewTransaction { get => _newTransaction; @@ -25,17 +29,29 @@ public TransactionViewModelItem NewTransaction } private ObservableCollection _transactions; + /// + /// Collection of loaded Transactions + /// public ObservableCollection Transactions { get => _transactions; set => Set(ref _transactions, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; + /// + /// Basic Constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month public TransactionViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) { _dbOptions = dbOptions; @@ -45,7 +61,11 @@ public TransactionViewModel(DbContextOptions dbOptions, YearMon //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { try { @@ -73,16 +93,24 @@ public async Task> LoadDataAsync() ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public async Task> LoadDataAsync(Bucket bucket, bool withMovements) + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed . Optionally will be transformed to + /// + /// Bucket for which Transactions should be loaded + /// Include which will be transformed to + /// Object which contains information and results of this method + public async Task LoadDataAsync(Bucket bucket, bool withMovements) { try { @@ -131,16 +159,23 @@ public async Task> LoadDataAsync(Bucket bucket, bool withMov ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public async Task> LoadDataAsync(Account account) + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed + /// + /// Account for which Transactions should be loaded + /// Object which contains information and results of this method + public async Task LoadDataAsync(Account account) { try { @@ -166,35 +201,44 @@ public async Task> LoadDataAsync(Account account) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple CreateItem() + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateItem() { - NewTransaction.Transaction.TransactionId = 0; // Triggers CREATE during CreateUpdateTransaction() - var (result, message) = NewTransaction.CreateItem(); - if (!result) - { - return new Tuple(false, message); - } + NewTransaction.Transaction.TransactionId = 0; + var result = NewTransaction.CreateItem(); + if (!result.IsSuccessful) return result; ResetNewTransaction(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } + /// + /// Helper method to reset values of + /// public void ResetNewTransaction() { NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel); - NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel)); + NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth)); } + /// + /// Helper method to start modification process for all Transactions + /// public void EditAllTransaction() { foreach (var transaction in Transactions) @@ -203,7 +247,11 @@ public void EditAllTransaction() } } - public Tuple SaveAllTransaction() + /// + /// Starts update process for all Transactions + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllTransaction() { using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { @@ -211,25 +259,32 @@ public Tuple SaveAllTransaction() { foreach (var transaction in Transactions) { - (bool success, string message) = transaction.UpdateItem(); - if (!success) throw new Exception(message); + var result = transaction.UpdateItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } dbTransaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all changes to all Transactions + /// public void CancelAllTransaction() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); } + /// + /// Starts process to propose the right for all Transactions + /// + /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket public void ProposeBuckets() { foreach (var transaction in Transactions) diff --git a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs index b953423..89d3736 100644 --- a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs @@ -8,18 +8,11 @@ namespace OpenBudgeteer.Core.ViewModels { public class YearMonthSelectorViewModel : ViewModelBase { - public YearMonthSelectorViewModel() - { - Months = new ObservableCollection(); - for (var i = 1; i < 13; i++) - { - Months.Add(i); - } - SelectedMonth = DateTime.Now.Month; - SelectedYear = DateTime.Now.Year; - } - private int _selectedMonth; + /// + /// Number of the current month + /// + /// Change triggers public int SelectedMonth { get => _selectedMonth; @@ -31,6 +24,10 @@ public int SelectedMonth } private int _selectedYear; + /// + /// Number of the current year + /// + /// Change triggers public int SelectedYear { get => _selectedYear; @@ -42,28 +39,65 @@ public int SelectedYear } private ObservableCollection _months; + /// + /// Helper collection which contains the number of all months + /// public ObservableCollection Months { get => _months; - set => Set(ref _months, value); + private set => Set(ref _months, value); } + /// + /// Returns the first day as based on and + /// public DateTime CurrentMonth => new DateTime(SelectedYear, SelectedMonth, 1); + /// + /// EventHandler which should be invoked once the a year and/or a month has been modified. To be used to trigger + /// ViewModel reloads which are dependent on this ViewModel + /// public event EventHandler SelectedYearMonthChanged; - private bool _yearMontIsChanging; + private bool _yearMontIsChanging; // prevents double invoke of SelectedYearMonthChanged + + /// + /// Basic constructor + /// + public YearMonthSelectorViewModel() + { + Months = new ObservableCollection(); + for (var i = 1; i < 13; i++) + { + Months.Add(i); + } + SelectedMonth = DateTime.Now.Month; + SelectedYear = DateTime.Now.Year; + } + /// + /// Moves to the previous month + /// + /// Triggers public void PreviousMonth() { UpdateYearMonth(CurrentMonth.AddMonths(-1)); } + /// + /// Moves to the next month + /// + /// Triggers public void NextMonth() { UpdateYearMonth(CurrentMonth.AddMonths(1)); } + /// + /// Sets the date to the passed + /// + /// New date + /// Triggers (only once) private void UpdateYearMonth(DateTime newYearMonth) { _yearMontIsChanging = true; diff --git a/OpenBudgeteer.sln b/OpenBudgeteer.sln index df48e7a..cba1bb4 100644 --- a/OpenBudgeteer.sln +++ b/OpenBudgeteer.sln @@ -3,15 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29424.173 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "OpenBudgeteer.Core", "OpenBudgeteer.Core\OpenBudgeteer.Core.shproj", "{CC22A1E2-FA88-417C-A402-69526C33D2F4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Blazor", "OpenBudgeteer.Blazor\OpenBudgeteer.Blazor.csproj", "{0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Core", "OpenBudgeteer.Core\OpenBudgeteer.Core.csproj", "{B2969681-AC0D-4570-9043-46DBDAF11B18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenBudgeteer.Core.Test", "OpenBudgeteer.Core.Test\OpenBudgeteer.Core.Test.csproj", "{2AFE22D2-18FA-4326-B011-B6E3D6365E6B}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - OpenBudgeteer.Core\OpenBudgeteer.Core.projitems*{0a5c2cad-5438-4d2c-9c17-1adf5c9aa9d3}*SharedItemsImports = 5 - OpenBudgeteer.Core\OpenBudgeteer.Core.projitems*{cc22a1e2-fa88-417c-a402-69526c33d2f4}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -21,6 +19,14 @@ Global {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Release|Any CPU.Build.0 = Release|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Release|Any CPU.Build.0 = Release|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index f23e6c5..c4803e3 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and ins ## Installation (Docker) -You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/r/axelander/openbudgeteer). It requires a connection to a MySQL database which can be achieved by passing the following variables: +You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/r/axelander/openbudgeteer). It requires a connection to a `MySQL` database which can be achieved by passing the following variables: | Variable | Description | Example | | --- | --- | --- | +| Connection:Provider | Type of database that should be used | mysql | | Connection:Server | IP Address to MySQL Server | 192.168.178.100 | | Connection:Port| Port to MySQL Server | 3306 | | Connection:Database | Database name | MyOpenBudgeteerDb | @@ -16,6 +17,7 @@ You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/ ``` docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ -e 'Connection:Port'='3306' \ -e 'Connection:Database'='MyOpenBudgeteerDb' \ @@ -25,6 +27,15 @@ docker run -d --name='openbudgeteer' \ 'axelander/openbudgeteer:latest' ``` +Alternatively you can use a local `Sqlite` database using the below settings: + +``` +docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='sqlite' \ + -v '/my/local/path:/app/database' \ + -p '6100:80/tcp' \ + 'axelander/openbudgeteer:latest' +``` If you don't change the Port Mapping you can access the App with Port `80`. Otherwise like above example it can be accessed with Port `6100` ### Pre-release Version (Docker) @@ -33,6 +44,7 @@ A Pre-Release version can be used with the Tag `pre-release` ``` docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ -e 'Connection:Port'='3306' \ -e 'Connection:Database'='MyOpenBudgeteerDb' \