PR C: mechanical cleanup (typos, packages, formatting, culture)

- Consolidate decimal.ToFormattedString into Schaad.Accounting.Formatting
- Rename EnsureFileExisits -> EnsureFileExists and
  SplitPredefiniton* -> SplitPredefinition* (class, interface, files, DI reg)
- AccountRepository: single-save year-rollover start-balance seed
- Drop legacy .NET Standard packages redundant on net9.0 and the stale
  Interfaces\Extensions\ folder entry
- Set culture via RequestLocalizationOptions middleware
- Plan: item 9 (unit of work) split off into a dedicated PR D

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-07-02 20:46:55 +02:00
parent 152d4c3d57
commit 8961bb234c
15 changed files with 82 additions and 80 deletions

View file

@ -97,6 +97,7 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full
- **PR A** — Phase 1 items 1–7 + CLAUDE.md fix. - **PR A** — Phase 1 items 1–7 + CLAUDE.md fix.
- **PR B** — Phase 1 item 8 (atomic writes + lock). - **PR B** — Phase 1 item 8 (atomic writes + lock).
- **PR C** — Phase 2 items 9 + 11 + 13 + 15 + 16 + 17. - **PR C** — Phase 2 mechanical cleanup: items 11, 13, 15, 16, 17.
- **PR D** — Phase 3 (async). - **PR D** — Phase 2 item 9: per-request unit of work / cache. Split off from PR C because it is invasive enough to warrant its own review.
- **PR E** — Phase 4 (tests) — done alongside PR C to lock in behavior. - **PR E** — Phase 3 (async).
- **PR F** — Phase 4 (tests) — done alongside PR D to lock in behavior.

View file

@ -0,0 +1,19 @@
using System.Globalization;
namespace Schaad.Accounting
{
public static class Formatting
{
private static readonly CultureInfo SwissCulture = CreateSwissCulture();
public static string ToFormattedString(this decimal value)
=> value.ToString("#,0.00", SwissCulture);
private static CultureInfo CreateSwissCulture()
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return culture;
}
}
}

View file

@ -1,9 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces namespace Schaad.Accounting.Interfaces
{ {
public interface ISplitPredefinitonRepository public interface ISplitPredefinitionRepository
{ {
List<SplitPredefinition> GetSplitPredefinitionList(); List<SplitPredefinition> GetSplitPredefinitionList();

View file

@ -4,17 +4,9 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RootNamespace>Schaad.Accounting</RootNamespace> <RootNamespace>Schaad.Accounting</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Schaad.Finance.Api"> <Reference Include="Schaad.Finance.Api">
<HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath> <HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Interfaces\Extensions\" />
</ItemGroup>
</Project> </Project>

View file

@ -13,7 +13,7 @@ namespace Schaad.Accounting.Repositories
public AccountRepository(ISettingsService settingsService) : base(settingsService) public AccountRepository(ISettingsService settingsService) : base(settingsService)
{ {
EnsureFileExisits(ACCOUNTS); EnsureAccountsFile();
} }
@ -79,35 +79,40 @@ namespace Schaad.Accounting.Repositories
SaveAccount(account); SaveAccount(account);
} }
private new void EnsureFileExisits(string file) private void EnsureAccountsFile()
{ {
string filePath = Path.Combine(settingsService.GetDbPath(), file); string filePath = Path.Combine(settingsService.GetDbPath(), ACCOUNTS);
if (File.Exists(filePath) == false) if (File.Exists(filePath))
{ {
var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), file); return;
if (File.Exists(lastYearFile)) }
File.Copy(lastYearFile, filePath);
// set start balance to last bank balance var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), ACCOUNTS);
if (file.IndexOf("Accounts") > -1) if (File.Exists(lastYearFile))
{
File.Copy(lastYearFile, filePath);
}
// Year rollover: seed start balances from the previous year's closing values.
// Bank accounts start at the last known bank balance; cash accounts start at zero.
var accounts = GetAccountList();
var modified = false;
foreach (var account in accounts.Where(a => a.Class == ClassIds.Activa))
{
if (account.LastBankBalance > 0)
{ {
var accounts = GetAccountList(); account.StartBalance = account.LastBankBalance;
foreach (var account in accounts.Where(a => a.Class == ClassIds.Activa)) modified = true;
{
// bank accounts
if (account.LastBankBalance > 0)
{
account.StartBalance = account.LastBankBalance;
SaveAccount(account);
}
// cash accounts
else if (account.Number < 1010)
{
account.StartBalance = 0;
SaveAccount(account);
}
}
} }
else if (account.Number < 1010)
{
account.StartBalance = 0;
modified = true;
}
}
if (modified)
{
Save(accounts, ACCOUNTS);
} }
} }
} }

View file

@ -22,7 +22,7 @@ namespace Schaad.Accounting.Repositories
this.settingsService = settingsService; this.settingsService = settingsService;
} }
protected void EnsureFileExisits(string fileName) protected void EnsureFileExists(string fileName)
{ {
string filePath = Path.Combine(settingsService.GetDbPath(), fileName); string filePath = Path.Combine(settingsService.GetDbPath(), fileName);
lock (GetLock(filePath)) lock (GetLock(filePath))

View file

@ -12,7 +12,7 @@ namespace Schaad.Accounting.Repositories
public BookingRuleRepository(ISettingsService settingsService) : base(settingsService) public BookingRuleRepository(ISettingsService settingsService) : base(settingsService)
{ {
EnsureFileExisits(BOOKING_RULES); EnsureFileExists(BOOKING_RULES);
} }
/// <summary> /// <summary>

View file

@ -12,7 +12,7 @@ namespace Schaad.Accounting.Repositories
public BookingTextRepository(ISettingsService settingsService) : base(settingsService) public BookingTextRepository(ISettingsService settingsService) : base(settingsService)
{ {
EnsureFileExisits(BOOKING_TEXTS); EnsureFileExists(BOOKING_TEXTS);
} }
/// <summary> /// <summary>

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
@ -6,17 +6,17 @@ using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories namespace Schaad.Accounting.Repositories
{ {
public class SplitPredefinitonRepository : BaseRepository, ISplitPredefinitonRepository public class SplitPredefinitionRepository : BaseRepository, ISplitPredefinitionRepository
{ {
private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml"; private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml";
public SplitPredefinitonRepository(ISettingsService settingsService) : base(settingsService) public SplitPredefinitionRepository(ISettingsService settingsService) : base(settingsService)
{ {
EnsureFileExisits(SPLIT_PREDEFINITION); EnsureFileExists(SPLIT_PREDEFINITION);
} }
/// <summary> /// <summary>
/// Load booking rules /// Load split predefinitions
/// </summary> /// </summary>
public List<SplitPredefinition> GetSplitPredefinitionList() public List<SplitPredefinition> GetSplitPredefinitionList()
{ {
@ -25,7 +25,7 @@ namespace Schaad.Accounting.Repositories
} }
/// <summary> /// <summary>
/// Save a booking rule (insert/update) /// Save a split predefinition (insert/update)
/// </summary> /// </summary>
public void SaveSplitPredefinition(SplitPredefinition splitPredefinition) public void SaveSplitPredefinition(SplitPredefinition splitPredefinition)
{ {

View file

@ -13,7 +13,7 @@ namespace Schaad.Accounting.Repositories
public SubclassRepository(ISettingsService settingsService) : base(settingsService) public SubclassRepository(ISettingsService settingsService) : base(settingsService)
{ {
EnsureFileExisits(SUBCLASSES); EnsureFileExists(SUBCLASSES);
classes.Add(ClassIds.Activa, "Aktiven"); classes.Add(ClassIds.Activa, "Aktiven");
classes.Add(ClassIds.Passiva, "Passiven"); classes.Add(ClassIds.Passiva, "Passiven");

View file

@ -4,9 +4,6 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" /> <ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />
</ItemGroup> </ItemGroup>

View file

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -134,25 +133,18 @@ namespace Schaad.Accounting.Services
if (trx.OriginAccountId == accountId) if (trx.OriginAccountId == accountId)
{ {
debit = ToFormattedString(trx.Value); debit = trx.Value.ToFormattedString();
signedValue = -trx.Value; signedValue = -trx.Value;
} }
else else
{ {
credit = ToFormattedString(trx.Value); credit = trx.Value.ToFormattedString();
} }
balance += signedValue; balance += signedValue;
sb.AppendLine($"{trx.BookingDate:dd.MM.yyyy};{trx.ValueDate:dd.MM.yyyy};{trx.Text};{debit};{credit};{ToFormattedString(balance)}"); sb.AppendLine($"{trx.BookingDate:dd.MM.yyyy};{trx.ValueDate:dd.MM.yyyy};{trx.Text};{debit};{credit};{balance.ToFormattedString()}");
} }
var fileBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(sb.ToString()); var fileBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(sb.ToString());
return fileBytes; return fileBytes;
} }
private string ToFormattedString(decimal value)
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return value.ToString("#,0.00", culture);
}
} }
} }

View file

@ -6,7 +6,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FreeSpire.PDF" Version="10.2.0" /> <PackageReference Include="FreeSpire.PDF" Version="10.2.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" /> <ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />

View file

@ -1,5 +1,4 @@
using System.Globalization; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Repositories; using Schaad.Accounting.Repositories;
using Schaad.Accounting.Services; using Schaad.Accounting.Services;
using Schaad.Finance.Api; using Schaad.Finance.Api;
@ -9,13 +8,6 @@ namespace Schaad.Accounting.UI
{ {
public static class Extensions public static class Extensions
{ {
public static string ToFormattedString(this decimal value)
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return value.ToString("#,0.00", culture);
}
public static IServiceCollection AddAccounting(this IServiceCollection services) public static IServiceCollection AddAccounting(this IServiceCollection services)
{ {
// Singleton because MyHeader triggers a full page reload (forceLoad: true) after // Singleton because MyHeader triggers a full page reload (forceLoad: true) after
@ -28,7 +20,7 @@ namespace Schaad.Accounting.UI
services.AddScoped<IBankTransactionRepository, BankTransactionRepository>(); services.AddScoped<IBankTransactionRepository, BankTransactionRepository>();
services.AddScoped<IBookingRuleRepository, BookingRuleRepository>(); services.AddScoped<IBookingRuleRepository, BookingRuleRepository>();
services.AddScoped<IBookingTextRepository, BookingTextRepository>(); services.AddScoped<IBookingTextRepository, BookingTextRepository>();
services.AddScoped<ISplitPredefinitonRepository, SplitPredefinitonRepository>(); services.AddScoped<ISplitPredefinitionRepository, SplitPredefinitionRepository>();
services.AddScoped<ISubclassRepository, SubclassRepository>(); services.AddScoped<ISubclassRepository, SubclassRepository>();
services.AddScoped<ITransactionRepository, TransactionRepository>(); services.AddScoped<ITransactionRepository, TransactionRepository>();

View file

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
@ -38,15 +39,19 @@ namespace Schaad.Accounting.UI
app.UseHttpsRedirection(); app.UseHttpsRedirection();
var supportedCultures = new[] { new CultureInfo("de-CH") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(supportedCultures[0]),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseAntiforgery(); app.UseAntiforgery();
app.MapStaticAssets(); app.MapStaticAssets();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
var culture = new CultureInfo("de-CH");
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
app.Run(); app.Run();
} }
} }