Implement backup and restore functionality; add IBackupService and BackupService; refactor services to use DbContextFactory
This commit is contained in:
35
.vscode/tasks.json
vendored
35
.vscode/tasks.json
vendored
@@ -22,6 +22,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Duempelkas.Desktop/Duempelkas.Desktop.csproj",
|
||||||
|
"-c",
|
||||||
|
"Release",
|
||||||
|
"-r",
|
||||||
|
"win-x64",
|
||||||
|
"--self-contained",
|
||||||
|
"true",
|
||||||
|
"/p:PublishSingleFile=true",
|
||||||
|
"/p:EnableCompressionInSingleFile=true",
|
||||||
|
"/p:IncludeNativeLibrariesForSelfExtract=true",
|
||||||
|
"/p:IncludeAllContentForSelfExtract=true",
|
||||||
|
"/p:DebugType=None",
|
||||||
|
"/p:DebugSymbols=false",
|
||||||
|
"-o",
|
||||||
|
"${workspaceFolder}/Publish"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"options": {
|
||||||
|
"statusbar": {
|
||||||
|
"label": "$(package) Publish",
|
||||||
|
"color": "#22c55e",
|
||||||
|
"detail": "Publish desktop app into Publish folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "watch",
|
"label": "watch",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
@@ -34,6 +65,10 @@
|
|||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile",
|
"problemMatcher": "$msCompile",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "never",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"statusbar": {
|
"statusbar": {
|
||||||
"label": "$(eye) Watch",
|
"label": "$(eye) Watch",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@using System.Reflection
|
||||||
|
|
||||||
<div class="d-flex flex-column vh-100">
|
<div class="d-flex flex-column vh-100">
|
||||||
<nav class="app-navbar d-flex justify-content-between align-items-center">
|
<nav class="app-navbar d-flex justify-content-between align-items-center">
|
||||||
@@ -43,10 +44,17 @@
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var entryAssembly = Assembly.GetEntryAssembly();
|
||||||
var yearShort = now.Year % 100;
|
var infoVersion = entryAssembly?
|
||||||
var dayOfYear = now.DayOfYear;
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||||
return $"{yearShort}.{dayOfYear}";
|
.InformationalVersion;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(infoVersion))
|
||||||
|
{
|
||||||
|
return infoVersion.Split('+')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entryAssembly?.GetName().Version?.ToString(2) ?? "1.0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<button class="btn btn-secondary btn-nav" @onclick="NavigateBack">
|
<button class="btn btn-secondary btn-nav" @onclick="NavigateBack">
|
||||||
<i class="bi bi-arrow-left"></i> Zurück
|
<i class="bi bi-arrow-left"></i> Zurück
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
<button class="btn btn-success btn-nav" @onclick="() => showAddEntry = true">
|
<button class="btn btn-success btn-nav" @onclick="() => showAddEntry = true">
|
||||||
<i class="bi bi-plus-lg"></i> Buchung
|
<i class="bi bi-plus-lg"></i> Buchung
|
||||||
</button>
|
</button>
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<button class="btn btn-dark btn-nav" @onclick="HandleExport">
|
<button class="btn btn-dark btn-nav" @onclick="HandleExport">
|
||||||
<i class="bi bi-file-earmark-pdf"></i> PDF
|
<i class="bi bi-file-earmark-pdf"></i> PDF
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
<button class="btn btn-nav @(showCurrentYearOnly ? "btn-primary" : "btn-outline-secondary")" @onclick="ToggleYearFilter">
|
<button class="btn btn-nav @(showCurrentYearOnly ? "btn-primary" : "btn-outline-secondary")" @onclick="ToggleYearFilter">
|
||||||
<i class="bi bi-funnel"></i> @(showCurrentYearOnly ? $"Nur {DateTime.Now.Year}" : "Alle Buchungen")
|
<i class="bi bi-funnel"></i> @(showCurrentYearOnly ? $"Nur {DateTime.Now.Year}" : "Alle Buchungen")
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IAccountService AccountService
|
@inject IAccountService AccountService
|
||||||
|
@inject IBackupService BackupService
|
||||||
@inject ISettingsService SettingsService
|
@inject ISettingsService SettingsService
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
@@ -19,8 +22,22 @@
|
|||||||
<button class="btn-nav btn-primary" @onclick="() => showAddAccount = true">
|
<button class="btn-nav btn-primary" @onclick="() => showAddAccount = true">
|
||||||
<i class="bi bi-plus-lg"></i> Neues Konto
|
<i class="bi bi-plus-lg"></i> Neues Konto
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
<button class="btn-nav btn-success" @onclick="HandleBackupAsync">
|
||||||
|
<i class="bi bi-save"></i> Backup
|
||||||
|
</button>
|
||||||
|
<button class="btn-nav btn-warning" @onclick="HandleRestoreAsync">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> Restore
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(operationMessage))
|
||||||
|
{
|
||||||
|
<div class="alert @operationMessageClass mb-3" role="alert">
|
||||||
|
@operationMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (accounts == null)
|
@if (accounts == null)
|
||||||
{
|
{
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
@@ -56,6 +73,8 @@
|
|||||||
private bool showAddAccount;
|
private bool showAddAccount;
|
||||||
private bool showEditClubName;
|
private bool showEditClubName;
|
||||||
private string clubName = string.Empty;
|
private string clubName = string.Empty;
|
||||||
|
private string? operationMessage;
|
||||||
|
private string operationMessageClass = "alert-info";
|
||||||
private string DisplayClubName => string.IsNullOrWhiteSpace(clubName) ? "Mein Verein" : clubName;
|
private string DisplayClubName => string.IsNullOrWhiteSpace(clubName) ? "Mein Verein" : clubName;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -87,4 +106,39 @@
|
|||||||
showEditClubName = false;
|
showEditClubName = false;
|
||||||
await LoadClubName();
|
await LoadClubName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleBackupAsync()
|
||||||
|
{
|
||||||
|
var message = await BackupService.CreateBackupAsync();
|
||||||
|
if(message.Contains("fehlgeschlagen", StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
SetOperationMessage(message, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRestoreAsync()
|
||||||
|
{
|
||||||
|
var confirmed = await JsRuntime.InvokeAsync<bool>("confirm",
|
||||||
|
"Restore überschreibt die aktuelle Datenbank. Möchten Sie fortfahren?");
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
var message = await BackupService.RestoreBackupAsync();
|
||||||
|
var isSuccess = message.StartsWith("Wiederherstellung erfolgreich", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Reload the Blazor app so all components/services re-query from restored DB.
|
||||||
|
NavigationManager.NavigateTo("/", forceLoad: true);
|
||||||
|
} else {
|
||||||
|
SetOperationMessage(message, isSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetOperationMessage(string message, bool success)
|
||||||
|
{
|
||||||
|
operationMessage = message;
|
||||||
|
operationMessageClass = success ? "alert-success" : "alert-danger";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Duempelkas.App/Services/IBackupService.cs
Normal file
7
src/Duempelkas.App/Services/IBackupService.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Duempelkas.App.Services;
|
||||||
|
|
||||||
|
public interface IBackupService
|
||||||
|
{
|
||||||
|
Task<string> CreateBackupAsync();
|
||||||
|
Task<string> RestoreBackupAsync();
|
||||||
|
}
|
||||||
@@ -2,8 +2,21 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
<OutputType Condition="$([MSBuild]::IsOSPlatform('Windows'))">WinExe</OutputType>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<Version>$([System.DateTime]::Now.ToString('yy')).$([System.DateTime]::Now.DayOfYear)</Version>
|
||||||
|
<PublishedExeFileName>Duempelkas-v$(Version).exe</PublishedExeFileName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
|
<DebugType>None</DebugType>
|
||||||
|
<DebugSymbols>false</DebugSymbols>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -24,4 +37,11 @@
|
|||||||
<ProjectReference Include="..\Duempelkas.App\Duempelkas.App.csproj" />
|
<ProjectReference Include="..\Duempelkas.App\Duempelkas.App.csproj" />
|
||||||
<ProjectReference Include="..\Duempelkas.Infrastructure\Duempelkas.Infrastructure.csproj" />
|
<ProjectReference Include="..\Duempelkas.Infrastructure\Duempelkas.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="FinalizePublishOutput" AfterTargets="Publish">
|
||||||
|
<Move SourceFiles="$(PublishDir)$(AssemblyName).exe"
|
||||||
|
DestinationFiles="$(PublishDir)$(PublishedExeFileName)"
|
||||||
|
Condition="Exists('$(PublishDir)$(AssemblyName).exe') and '$(AssemblyName).exe' != '$(PublishedExeFileName)'" />
|
||||||
|
<Touch Files="$(PublishDir)duempelkas.db" AlwaysCreate="true" Condition="!Exists('$(PublishDir)duempelkas.db')" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class Program
|
|||||||
{
|
{
|
||||||
var appBuilder = PhotinoBlazorAppBuilder.CreateDefault(args);
|
var appBuilder = PhotinoBlazorAppBuilder.CreateDefault(args);
|
||||||
|
|
||||||
var dbPath = Path.Combine(AppContext.BaseDirectory, "duempelkas.db");
|
var exeDirectory = Path.GetDirectoryName(Environment.ProcessPath ?? AppContext.BaseDirectory) ?? AppContext.BaseDirectory;
|
||||||
|
var dbPath = Path.Combine(exeDirectory, "duempelkas.db");
|
||||||
appBuilder.Services.AddInfrastructure($"Data Source={dbPath}");
|
appBuilder.Services.AddInfrastructure($"Data Source={dbPath}");
|
||||||
|
|
||||||
appBuilder.RootComponents.Add<Duempelkas.App.Components.App>("app");
|
appBuilder.RootComponents.Add<Duempelkas.App.Components.App>("app");
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
services.AddDbContext<FinanceDbContext>(options =>
|
services.AddDbContext<FinanceDbContext>(options =>
|
||||||
options.UseSqlite(connectionString));
|
options.UseSqlite(connectionString));
|
||||||
|
services.AddDbContextFactory<FinanceDbContext>(options =>
|
||||||
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
services.AddScoped<IAccountService, AccountService>();
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
services.AddScoped<IEntryService, EntryService>();
|
services.AddScoped<IEntryService, EntryService>();
|
||||||
services.AddScoped<IBalanceQueryService, BalanceQueryService>();
|
services.AddScoped<IBalanceQueryService, BalanceQueryService>();
|
||||||
services.AddScoped<IPdfStatementService, PdfStatementService>();
|
services.AddScoped<IPdfStatementService, PdfStatementService>();
|
||||||
services.AddScoped<IFileSaveService, FileSaveService>();
|
services.AddScoped<IFileSaveService, FileSaveService>();
|
||||||
|
services.AddSingleton<IBackupService, BackupService>();
|
||||||
services.AddSingleton<ISettingsService, SettingsService>();
|
services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ namespace Duempelkas.Infrastructure.Services;
|
|||||||
|
|
||||||
public class AccountService : IAccountService
|
public class AccountService : IAccountService
|
||||||
{
|
{
|
||||||
private readonly FinanceDbContext _db;
|
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
|
||||||
|
|
||||||
public AccountService(FinanceDbContext db) => _db = db;
|
public AccountService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
|
||||||
|
|
||||||
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync()
|
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync()
|
||||||
{
|
{
|
||||||
var accounts = await _db.Accounts
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var accounts = await db.Accounts
|
||||||
.Include(a => a.Entries)
|
.Include(a => a.Entries)
|
||||||
.OrderBy(a => a.Name)
|
.OrderBy(a => a.Name)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -25,7 +27,9 @@ public class AccountService : IAccountService
|
|||||||
|
|
||||||
public async Task<AccountSummaryDto> GetAccountAsync(int accountId)
|
public async Task<AccountSummaryDto> GetAccountAsync(int accountId)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts
|
||||||
.Include(a => a.Entries)
|
.Include(a => a.Entries)
|
||||||
.FirstOrDefaultAsync(a => a.Id == accountId)
|
.FirstOrDefaultAsync(a => a.Id == accountId)
|
||||||
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
||||||
@@ -35,51 +39,59 @@ public class AccountService : IAccountService
|
|||||||
|
|
||||||
public async Task<AccountSummaryDto> CreateAccountAsync(string name)
|
public async Task<AccountSummaryDto> CreateAccountAsync(string name)
|
||||||
{
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var account = new Account { Name = name };
|
var account = new Account { Name = name };
|
||||||
_db.Accounts.Add(account);
|
db.Accounts.Add(account);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return new AccountSummaryDto(account.Id, account.Name, 0m, 0m, account.CreatedUtc);
|
return new AccountSummaryDto(account.Id, account.Name, 0m, 0m, account.CreatedUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RenameAccountAsync(int accountId, string newName)
|
public async Task RenameAccountAsync(int accountId, string newName)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts.FindAsync(accountId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts.FindAsync(accountId)
|
||||||
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
||||||
account.Name = newName;
|
account.Name = newName;
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance)
|
public async Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts.FindAsync(accountId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts.FindAsync(accountId)
|
||||||
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
||||||
account.CarryoverBalance = carryoverBalance;
|
account.CarryoverBalance = carryoverBalance;
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAccountAsync(int accountId)
|
public async Task DeleteAccountAsync(int accountId)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts
|
||||||
.Include(a => a.Entries)
|
.Include(a => a.Entries)
|
||||||
.FirstOrDefaultAsync(a => a.Id == accountId)
|
.FirstOrDefaultAsync(a => a.Id == accountId)
|
||||||
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
?? throw new InvalidOperationException($"Account {accountId} not found.");
|
||||||
|
|
||||||
var entryIds = account.Entries.Select(e => e.Id).ToList();
|
var entryIds = account.Entries.Select(e => e.Id).ToList();
|
||||||
var transferLinks = await _db.TransferLinks
|
var transferLinks = await db.TransferLinks
|
||||||
.Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId))
|
.Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
foreach (var link in transferLinks)
|
foreach (var link in transferLinks)
|
||||||
{
|
{
|
||||||
var otherEntryId = entryIds.Contains(link.SourceEntryId) ? link.TargetEntryId : link.SourceEntryId;
|
var otherEntryId = entryIds.Contains(link.SourceEntryId) ? link.TargetEntryId : link.SourceEntryId;
|
||||||
var otherEntry = await _db.Entries.FindAsync(otherEntryId);
|
var otherEntry = await db.Entries.FindAsync(otherEntryId);
|
||||||
if (otherEntry != null)
|
if (otherEntry != null)
|
||||||
_db.Entries.Remove(otherEntry);
|
db.Entries.Remove(otherEntry);
|
||||||
}
|
}
|
||||||
_db.TransferLinks.RemoveRange(transferLinks);
|
db.TransferLinks.RemoveRange(transferLinks);
|
||||||
|
|
||||||
_db.Accounts.Remove(account);
|
db.Accounts.Remove(account);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AccountSummaryDto MapToSummary(Account account)
|
private static AccountSummaryDto MapToSummary(Account account)
|
||||||
|
|||||||
146
src/Duempelkas.Infrastructure/Services/BackupService.cs
Normal file
146
src/Duempelkas.Infrastructure/Services/BackupService.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Duempelkas.App.Services;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Duempelkas.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class BackupService : IBackupService
|
||||||
|
{
|
||||||
|
private static readonly string DbPath = Path.Combine(
|
||||||
|
Path.GetDirectoryName(Environment.ProcessPath ?? AppContext.BaseDirectory) ?? AppContext.BaseDirectory,
|
||||||
|
"duempelkas.db");
|
||||||
|
|
||||||
|
public async Task<string> CreateBackupAsync()
|
||||||
|
{
|
||||||
|
if (!File.Exists(DbPath))
|
||||||
|
{
|
||||||
|
return "Keine Datenbankdatei gefunden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var suggestedFileName = $"duempelkas-backup-{DateTime.Now:yyMMdd-HHmm}.bak";
|
||||||
|
var backupPath = await ShowSaveDialogAsync(suggestedFileName);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(backupPath))
|
||||||
|
{
|
||||||
|
return "Backup wurde abgebrochen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPath = Path.ChangeExtension(backupPath, ".bak");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(DbPath, backupPath, overwrite: true);
|
||||||
|
return $"Backup gespeichert: {backupPath}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"Backup fehlgeschlagen: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> RestoreBackupAsync()
|
||||||
|
{
|
||||||
|
var backupPath = await ShowOpenDialogAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(backupPath))
|
||||||
|
{
|
||||||
|
return "Wiederherstellung wurde abgebrochen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(backupPath))
|
||||||
|
{
|
||||||
|
return "Die gewählte Backup-Datei wurde nicht gefunden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure no pooled connection keeps stale handles to the previous DB file.
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
|
||||||
|
var targetDirectory = Path.GetDirectoryName(DbPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(targetDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(backupPath, DbPath, overwrite: true);
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
return "Wiederherstellung erfolgreich. Die Datenbank wurde neu geladen.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"Wiederherstellung fehlgeschlagen: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<string?> ShowSaveDialogAsync(string suggestedFileName)
|
||||||
|
{
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||||
|
return Path.Combine(documentsPath, suggestedFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var script = $@"
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$dialog = New-Object System.Windows.Forms.SaveFileDialog
|
||||||
|
$dialog.Filter = 'Backup-Dateien (*.bak)|*.bak'
|
||||||
|
$dialog.DefaultExt = 'bak'
|
||||||
|
$dialog.AddExtension = $true
|
||||||
|
$dialog.OverwritePrompt = $true
|
||||||
|
$dialog.FileName = '{suggestedFileName.Replace("'", "''")}'
|
||||||
|
$dialog.Title = 'Backup speichern'
|
||||||
|
if ($dialog.ShowDialog() -eq 'OK') {{ $dialog.FileName }} else {{ '' }}
|
||||||
|
";
|
||||||
|
|
||||||
|
return RunPowerShellDialog(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<string?> ShowOpenDialogAsync()
|
||||||
|
{
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var script = @"
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||||
|
$dialog.Filter = 'Backup-Dateien (*.bak)|*.bak'
|
||||||
|
$dialog.Multiselect = $false
|
||||||
|
$dialog.Title = 'Backup auswählen'
|
||||||
|
if ($dialog.ShowDialog() -eq 'OK') { $dialog.FileName } else { '' }
|
||||||
|
";
|
||||||
|
|
||||||
|
return RunPowerShellDialog(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? RunPowerShellDialog(string script)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "powershell",
|
||||||
|
Arguments = $"-NoProfile -Command \"{script.Replace("\"", "\\\"") }\"",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = Process.Start(psi);
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = process.StandardOutput.ReadToEnd().Trim();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(result) ? null : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,15 @@ namespace Duempelkas.Infrastructure.Services;
|
|||||||
|
|
||||||
public class BalanceQueryService : IBalanceQueryService
|
public class BalanceQueryService : IBalanceQueryService
|
||||||
{
|
{
|
||||||
private readonly FinanceDbContext _db;
|
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
|
||||||
|
|
||||||
public BalanceQueryService(FinanceDbContext db) => _db = db;
|
public BalanceQueryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
|
||||||
|
|
||||||
public async Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId)
|
public async Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts
|
||||||
.Include(a => a.Entries)
|
.Include(a => a.Entries)
|
||||||
.FirstOrDefaultAsync(a => a.Id == accountId)
|
.FirstOrDefaultAsync(a => a.Id == accountId)
|
||||||
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ namespace Duempelkas.Infrastructure.Services;
|
|||||||
|
|
||||||
public class EntryService : IEntryService
|
public class EntryService : IEntryService
|
||||||
{
|
{
|
||||||
private readonly FinanceDbContext _db;
|
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
|
||||||
|
|
||||||
public EntryService(FinanceDbContext db) => _db = db;
|
public EntryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
|
||||||
|
|
||||||
public async Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly)
|
public async Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly)
|
||||||
{
|
{
|
||||||
var query = _db.Entries.Where(e => e.AccountId == accountId);
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var query = db.Entries.Where(e => e.AccountId == accountId);
|
||||||
|
|
||||||
if (currentYearOnly)
|
if (currentYearOnly)
|
||||||
query = query.Where(e => e.Date.Year == DateTime.Now.Year);
|
query = query.Where(e => e.Date.Year == DateTime.Now.Year);
|
||||||
@@ -27,7 +29,7 @@ public class EntryService : IEntryService
|
|||||||
|
|
||||||
var entryIds = entries.Select(e => e.Id).ToList();
|
var entryIds = entries.Select(e => e.Id).ToList();
|
||||||
|
|
||||||
var transferLinks = await _db.TransferLinks
|
var transferLinks = await db.TransferLinks
|
||||||
.Include(tl => tl.SourceEntry).ThenInclude(e => e.Account)
|
.Include(tl => tl.SourceEntry).ThenInclude(e => e.Account)
|
||||||
.Include(tl => tl.TargetEntry).ThenInclude(e => e.Account)
|
.Include(tl => tl.TargetEntry).ThenInclude(e => e.Account)
|
||||||
.Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId))
|
.Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId))
|
||||||
@@ -59,7 +61,9 @@ public class EntryService : IEntryService
|
|||||||
|
|
||||||
public async Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount)
|
public async Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount)
|
||||||
{
|
{
|
||||||
var displayId = await GenerateDisplayIdAsync(accountId, date.Year);
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var displayId = await GenerateDisplayIdAsync(db, accountId, date.Year);
|
||||||
|
|
||||||
var entry = new Entry
|
var entry = new Entry
|
||||||
{
|
{
|
||||||
@@ -70,8 +74,8 @@ public class EntryService : IEntryService
|
|||||||
Title = title,
|
Title = title,
|
||||||
Amount = amount
|
Amount = amount
|
||||||
};
|
};
|
||||||
_db.Entries.Add(entry);
|
db.Entries.Add(entry);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return new EntryDto(entry.Id, entry.AccountId, entry.DisplayId, entry.Type, entry.Date, entry.Title, entry.Amount, false, false, null, null, null);
|
return new EntryDto(entry.Id, entry.AccountId, entry.DisplayId, entry.Type, entry.Date, entry.Title, entry.Amount, false, false, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -81,9 +85,10 @@ public class EntryService : IEntryService
|
|||||||
if (sourceAccountId == targetAccountId)
|
if (sourceAccountId == targetAccountId)
|
||||||
throw new InvalidOperationException("Umbuchung innerhalb desselben Kontos ist nicht möglich.");
|
throw new InvalidOperationException("Umbuchung innerhalb desselben Kontos ist nicht möglich.");
|
||||||
|
|
||||||
await using var transaction = await _db.Database.BeginTransactionAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
var sourceDisplayId = await GenerateDisplayIdAsync(sourceAccountId, date.Year);
|
var sourceDisplayId = await GenerateDisplayIdAsync(db, sourceAccountId, date.Year);
|
||||||
|
|
||||||
var sourceEntry = new Entry
|
var sourceEntry = new Entry
|
||||||
{
|
{
|
||||||
@@ -95,10 +100,10 @@ public class EntryService : IEntryService
|
|||||||
Amount = amount
|
Amount = amount
|
||||||
};
|
};
|
||||||
|
|
||||||
_db.Entries.Add(sourceEntry);
|
db.Entries.Add(sourceEntry);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var targetDisplayId = await GenerateDisplayIdAsync(targetAccountId, date.Year);
|
var targetDisplayId = await GenerateDisplayIdAsync(db, targetAccountId, date.Year);
|
||||||
|
|
||||||
var targetEntry = new Entry
|
var targetEntry = new Entry
|
||||||
{
|
{
|
||||||
@@ -110,8 +115,8 @@ public class EntryService : IEntryService
|
|||||||
Amount = amount
|
Amount = amount
|
||||||
};
|
};
|
||||||
|
|
||||||
_db.Entries.Add(targetEntry);
|
db.Entries.Add(targetEntry);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var link = new TransferLink
|
var link = new TransferLink
|
||||||
{
|
{
|
||||||
@@ -119,62 +124,68 @@ public class EntryService : IEntryService
|
|||||||
TargetEntryId = targetEntry.Id,
|
TargetEntryId = targetEntry.Id,
|
||||||
Note = $"Umbuchung: {title}"
|
Note = $"Umbuchung: {title}"
|
||||||
};
|
};
|
||||||
_db.TransferLinks.Add(link);
|
db.TransferLinks.Add(link);
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
sourceEntry.TransferLinkId = link.Id;
|
sourceEntry.TransferLinkId = link.Id;
|
||||||
targetEntry.TransferLinkId = link.Id;
|
targetEntry.TransferLinkId = link.Id;
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await transaction.CommitAsync();
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteEntryAsync(int entryId)
|
public async Task DeleteEntryAsync(int entryId)
|
||||||
{
|
{
|
||||||
var entry = await _db.Entries.FindAsync(entryId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var entry = await db.Entries.FindAsync(entryId)
|
||||||
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
||||||
|
|
||||||
entry.IsDeleted = true;
|
entry.IsDeleted = true;
|
||||||
|
|
||||||
var link = await _db.TransferLinks
|
var link = await db.TransferLinks
|
||||||
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
||||||
|
|
||||||
if (link != null)
|
if (link != null)
|
||||||
{
|
{
|
||||||
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
||||||
var otherEntry = await _db.Entries.FindAsync(otherEntryId);
|
var otherEntry = await db.Entries.FindAsync(otherEntryId);
|
||||||
if (otherEntry != null) otherEntry.IsDeleted = true;
|
if (otherEntry != null) otherEntry.IsDeleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RestoreEntryAsync(int entryId)
|
public async Task RestoreEntryAsync(int entryId)
|
||||||
{
|
{
|
||||||
var entry = await _db.Entries.FindAsync(entryId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var entry = await db.Entries.FindAsync(entryId)
|
||||||
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
||||||
|
|
||||||
entry.IsDeleted = false;
|
entry.IsDeleted = false;
|
||||||
|
|
||||||
var link = await _db.TransferLinks
|
var link = await db.TransferLinks
|
||||||
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
||||||
|
|
||||||
if (link != null)
|
if (link != null)
|
||||||
{
|
{
|
||||||
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
||||||
var otherEntry = await _db.Entries.FindAsync(otherEntryId);
|
var otherEntry = await db.Entries.FindAsync(otherEntryId);
|
||||||
if (otherEntry != null) otherEntry.IsDeleted = false;
|
if (otherEntry != null) otherEntry.IsDeleted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount)
|
public async Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount)
|
||||||
{
|
{
|
||||||
var entry = await _db.Entries.FindAsync(entryId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var entry = await db.Entries.FindAsync(entryId)
|
||||||
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
||||||
|
|
||||||
var link = await _db.TransferLinks
|
var link = await db.TransferLinks
|
||||||
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId);
|
||||||
|
|
||||||
entry.Date = date;
|
entry.Date = date;
|
||||||
@@ -184,7 +195,7 @@ public class EntryService : IEntryService
|
|||||||
if (link != null)
|
if (link != null)
|
||||||
{
|
{
|
||||||
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
||||||
var otherEntry = await _db.Entries.FindAsync(otherEntryId);
|
var otherEntry = await db.Entries.FindAsync(otherEntryId);
|
||||||
if (otherEntry != null)
|
if (otherEntry != null)
|
||||||
{
|
{
|
||||||
otherEntry.Date = date;
|
otherEntry.Date = date;
|
||||||
@@ -193,20 +204,22 @@ public class EntryService : IEntryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateTransferAsync(int entryId, int newLinkedAccountId, DateTime date, string title, decimal amount)
|
public async Task UpdateTransferAsync(int entryId, int newLinkedAccountId, DateTime date, string title, decimal amount)
|
||||||
{
|
{
|
||||||
var entry = await _db.Entries.FindAsync(entryId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var entry = await db.Entries.FindAsync(entryId)
|
||||||
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden.");
|
||||||
|
|
||||||
var link = await _db.TransferLinks
|
var link = await db.TransferLinks
|
||||||
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId)
|
.FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId)
|
||||||
?? throw new InvalidOperationException($"Kein Transfer-Link für Eintrag {entryId} gefunden.");
|
?? throw new InvalidOperationException($"Kein Transfer-Link für Eintrag {entryId} gefunden.");
|
||||||
|
|
||||||
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId;
|
||||||
var otherEntry = await _db.Entries.FindAsync(otherEntryId)
|
var otherEntry = await db.Entries.FindAsync(otherEntryId)
|
||||||
?? throw new InvalidOperationException($"Gegeneintrag {otherEntryId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Gegeneintrag {otherEntryId} nicht gefunden.");
|
||||||
|
|
||||||
// Update date, title, amount on both sides
|
// Update date, title, amount on both sides
|
||||||
@@ -221,16 +234,16 @@ public class EntryService : IEntryService
|
|||||||
if (otherEntry.AccountId != newLinkedAccountId)
|
if (otherEntry.AccountId != newLinkedAccountId)
|
||||||
{
|
{
|
||||||
otherEntry.AccountId = newLinkedAccountId;
|
otherEntry.AccountId = newLinkedAccountId;
|
||||||
otherEntry.DisplayId = await GenerateDisplayIdAsync(newLinkedAccountId, date.Year);
|
otherEntry.DisplayId = await GenerateDisplayIdAsync(db, newLinkedAccountId, date.Year);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GenerateDisplayIdAsync(int accountId, int year)
|
private static async Task<string> GenerateDisplayIdAsync(FinanceDbContext db, int accountId, int year)
|
||||||
{
|
{
|
||||||
var prefix = $"{year}-";
|
var prefix = $"{year}-";
|
||||||
var maxDisplayId = await _db.Entries
|
var maxDisplayId = await db.Entries
|
||||||
.Where(e => e.DisplayId.StartsWith(prefix))
|
.Where(e => e.DisplayId.StartsWith(prefix))
|
||||||
.Select(e => e.DisplayId)
|
.Select(e => e.DisplayId)
|
||||||
.MaxAsync(id => (string?)id);
|
.MaxAsync(id => (string?)id);
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ namespace Duempelkas.Infrastructure.Services;
|
|||||||
|
|
||||||
public class PdfStatementService : IPdfStatementService
|
public class PdfStatementService : IPdfStatementService
|
||||||
{
|
{
|
||||||
private readonly FinanceDbContext _db;
|
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
|
||||||
private readonly IEntryService _entryService;
|
private readonly IEntryService _entryService;
|
||||||
private readonly IBalanceQueryService _balanceQueryService;
|
private readonly IBalanceQueryService _balanceQueryService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private static readonly CultureInfo DeLocale = new("de-DE");
|
private static readonly CultureInfo DeLocale = new("de-DE");
|
||||||
|
|
||||||
public PdfStatementService(FinanceDbContext db, IEntryService entryService, IBalanceQueryService balanceQueryService, ISettingsService settingsService)
|
public PdfStatementService(IDbContextFactory<FinanceDbContext> dbFactory, IEntryService entryService, IBalanceQueryService balanceQueryService, ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_dbFactory = dbFactory;
|
||||||
_entryService = entryService;
|
_entryService = entryService;
|
||||||
_balanceQueryService = balanceQueryService;
|
_balanceQueryService = balanceQueryService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
@@ -28,7 +28,9 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
|
|
||||||
public async Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly)
|
public async Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly)
|
||||||
{
|
{
|
||||||
var account = await _db.Accounts.FindAsync(accountId)
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var account = await db.Accounts.FindAsync(accountId)
|
||||||
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
|
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
|
||||||
|
|
||||||
var entries = await _entryService.GetEntriesAsync(accountId, currentYearOnly);
|
var entries = await _entryService.GetEntriesAsync(accountId, currentYearOnly);
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ public class TransferServiceTests : IDisposable
|
|||||||
public TransferServiceTests()
|
public TransferServiceTests()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<FinanceDbContext>()
|
var options = new DbContextOptionsBuilder<FinanceDbContext>()
|
||||||
.UseSqlite("Data Source=:memory:")
|
.UseSqlite("Data Source=duempelkas-transfer-tests;Mode=Memory;Cache=Shared")
|
||||||
.Options;
|
.Options;
|
||||||
|
|
||||||
_db = new FinanceDbContext(options);
|
_db = new FinanceDbContext(options);
|
||||||
_db.Database.OpenConnection();
|
_db.Database.OpenConnection();
|
||||||
_db.Database.EnsureCreated();
|
_db.Database.EnsureCreated();
|
||||||
|
|
||||||
_entryService = new EntryService(_db);
|
var dbFactory = new TestDbContextFactory(options);
|
||||||
_balanceQueryService = new BalanceQueryService(_db);
|
|
||||||
|
_entryService = new EntryService(dbFactory);
|
||||||
|
_balanceQueryService = new BalanceQueryService(dbFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -77,6 +79,7 @@ public class TransferServiceTests : IDisposable
|
|||||||
|
|
||||||
await _entryService.DeleteEntryAsync(sourceEntry.Id);
|
await _entryService.DeleteEntryAsync(sourceEntry.Id);
|
||||||
|
|
||||||
|
_db.ChangeTracker.Clear();
|
||||||
var entries = await _db.Entries.ToListAsync();
|
var entries = await _db.Entries.ToListAsync();
|
||||||
entries.Should().HaveCount(2);
|
entries.Should().HaveCount(2);
|
||||||
entries.Should().AllSatisfy(e => e.IsDeleted.Should().BeTrue());
|
entries.Should().AllSatisfy(e => e.IsDeleted.Should().BeTrue());
|
||||||
@@ -107,4 +110,19 @@ public class TransferServiceTests : IDisposable
|
|||||||
_db.Database.CloseConnection();
|
_db.Database.CloseConnection();
|
||||||
_db.Dispose();
|
_db.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<FinanceDbContext>
|
||||||
|
{
|
||||||
|
private readonly DbContextOptions<FinanceDbContext> _options;
|
||||||
|
|
||||||
|
public TestDbContextFactory(DbContextOptions<FinanceDbContext> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FinanceDbContext CreateDbContext() => new(_options);
|
||||||
|
|
||||||
|
public Task<FinanceDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new FinanceDbContext(_options));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ public class BalanceCalculationTests : IDisposable
|
|||||||
public BalanceCalculationTests()
|
public BalanceCalculationTests()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<FinanceDbContext>()
|
var options = new DbContextOptionsBuilder<FinanceDbContext>()
|
||||||
.UseSqlite("Data Source=:memory:")
|
.UseSqlite("Data Source=duempelkas-balance-tests;Mode=Memory;Cache=Shared")
|
||||||
.Options;
|
.Options;
|
||||||
|
|
||||||
_db = new FinanceDbContext(options);
|
_db = new FinanceDbContext(options);
|
||||||
_db.Database.OpenConnection();
|
_db.Database.OpenConnection();
|
||||||
_db.Database.EnsureCreated();
|
_db.Database.EnsureCreated();
|
||||||
|
|
||||||
_balanceQueryService = new BalanceQueryService(_db);
|
var dbFactory = new TestDbContextFactory(options);
|
||||||
_entryService = new EntryService(_db);
|
|
||||||
|
_balanceQueryService = new BalanceQueryService(dbFactory);
|
||||||
|
_entryService = new EntryService(dbFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -106,4 +108,19 @@ public class BalanceCalculationTests : IDisposable
|
|||||||
_db.Database.CloseConnection();
|
_db.Database.CloseConnection();
|
||||||
_db.Dispose();
|
_db.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<FinanceDbContext>
|
||||||
|
{
|
||||||
|
private readonly DbContextOptions<FinanceDbContext> _options;
|
||||||
|
|
||||||
|
public TestDbContextFactory(DbContextOptions<FinanceDbContext> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FinanceDbContext CreateDbContext() => new(_options);
|
||||||
|
|
||||||
|
public Task<FinanceDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new FinanceDbContext(_options));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user