diff --git a/.github/prompts/01-solution-skeleton.prompt.md b/.github/prompts/01-solution-skeleton.prompt.md new file mode 100644 index 0000000..1672f7d --- /dev/null +++ b/.github/prompts/01-solution-skeleton.prompt.md @@ -0,0 +1,35 @@ +--- +description: "Create the Duempelkas .NET 10 solution skeleton with Photino.Blazor desktop host, Razor class library, domain, infrastructure, and test projects" +agent: "agent" +--- + +# Step 1: Solution Skeleton + +Create the complete .NET 10 solution structure for the Duempelkas cashbook/personal-finance application. + +## Projects + +| Project | SDK | Output | NuGet Packages | Project References | +|---------|-----|--------|----------------|--------------------| +| `src/Duempelkas.Desktop` | Microsoft.NET.Sdk | Exe | Photino.Blazor | App, Infrastructure | +| `src/Duempelkas.App` | Microsoft.NET.Sdk.Razor | Library | — | Domain | +| `src/Duempelkas.Domain` | Microsoft.NET.Sdk | Library | — | — | +| `src/Duempelkas.Infrastructure` | Microsoft.NET.Sdk | Library | Microsoft.EntityFrameworkCore.Sqlite, Microsoft.EntityFrameworkCore.Design, QuestPDF | Domain, App | +| `tests/Duempelkas.Tests` | Microsoft.NET.Sdk | Library | xunit, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk, FluentAssertions | Domain, Infrastructure, App | + +All projects target `net10.0`, enable nullable reference types, and use implicit usings. + +## Key Files to Create + +- `src/Duempelkas.Desktop/Program.cs` — Photino.Blazor startup: register DI services (DbContext, application services), register root Blazor component ``, configure window title and size, handle unhandled exceptions. +- `src/Duempelkas.Desktop/wwwroot/index.html` — HTML host page with Bootstrap 5 CDN, app CSS link, and `` root element. +- `src/Duempelkas.App/Components/App.razor` — Root Blazor component with Router. +- `src/Duempelkas.App/Components/Routes.razor` — Router pointing to `Duempelkas.App.Pages` assembly. +- `src/Duempelkas.App/Components/Layout/MainLayout.razor` — Shell layout with sidebar/nav and main content area. +- `src/Duempelkas.App/_Imports.razor` — Global usings for Microsoft.AspNetCore.Components, Duempelkas namespaces. + +## Conventions + +- File-scoped namespaces everywhere. +- Root namespace matches folder structure: `Duempelkas.Desktop`, `Duempelkas.App`, etc. +- Place the `.sln` file at the repository root. diff --git a/.github/prompts/02-domain-model.prompt.md b/.github/prompts/02-domain-model.prompt.md new file mode 100644 index 0000000..8fb50e8 --- /dev/null +++ b/.github/prompts/02-domain-model.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Implement the EF Core entity classes and enums for Account, AccountYear, Entry, TransferLink, and EntryType" +agent: "agent" +--- + +# Step 2: Domain Model + +Implement the domain entities under `src/Duempelkas.Domain`. + +## Entities + +### Account (`Entities/Account.cs`) +- `int Id` (PK) +- `string Name` (required, max 200) +- `DateTime CreatedUtc` +- `ICollection AccountYears` (navigation) + +### AccountYear (`Entities/AccountYear.cs`) +- `int Id` (PK) +- `int AccountId` (FK → Account) +- `int Year` (e.g. 2026) +- `decimal OpeningBalance` — explicit carryover from the previous year, user-entered +- `DateTime CreatedUtc` +- `Account Account` (navigation) +- `ICollection Entries` (navigation) + +Constraint: unique on `(AccountId, Year)`. + +### Entry (`Entities/Entry.cs`) +- `int Id` (PK) +- `int AccountYearId` (FK → AccountYear) +- `EntryType Type` — Income or Expense +- `DateTime Date` +- `string Title` (required, max 500) +- `decimal Amount` — always positive; sign determined by Type +- `int? TransferLinkId` (FK → TransferLink, nullable) +- `DateTime CreatedUtc` +- `AccountYear AccountYear` (navigation) +- `TransferLink? TransferLink` (navigation) + +### TransferLink (`Entities/TransferLink.cs`) +- `int Id` (PK) +- `int SourceEntryId` (FK → Entry, unique) +- `int TargetEntryId` (FK → Entry, unique) +- `string? Note` +- `DateTime CreatedUtc` +- `Entry SourceEntry` (navigation) +- `Entry TargetEntry` (navigation) + +Invariants enforced at service level: +- SourceEntry.Type must be Expense +- TargetEntry.Type must be Income +- Both share the same Amount and Date + +### EntryType (`Enums/EntryType.cs`) +```csharp +public enum EntryType { Income = 0, Expense = 1 } +``` + +## Conventions +- Use `decimal` for all monetary amounts. +- Store amounts as positive; behaviour (add/subtract) comes from `EntryType`. +- File-scoped namespaces, nullable enabled. diff --git a/.github/prompts/03-dbcontext-mappings.prompt.md b/.github/prompts/03-dbcontext-mappings.prompt.md new file mode 100644 index 0000000..a6e422c --- /dev/null +++ b/.github/prompts/03-dbcontext-mappings.prompt.md @@ -0,0 +1,44 @@ +--- +description: "Implement FinanceDbContext with fluent entity configurations, indexes, relationships, and SQLite connection setup" +agent: "agent" +--- + +# Step 3: DbContext & Mappings + +Create `src/Duempelkas.Infrastructure/Persistence/FinanceDbContext.cs`. + +## DbSets +- `DbSet Accounts` +- `DbSet AccountYears` +- `DbSet Entries` +- `DbSet TransferLinks` + +## Connection +- Accept SQLite connection string via constructor options. +- Default path: `Data Source=duempelkas.db` in the app's base directory. + +## Entity Configurations (use `OnModelCreating` with `IEntityTypeConfiguration` or inline) + +### Account +- `Name` required, max length 200. +- Cascade delete → AccountYears. + +### AccountYear +- Unique index on `(AccountId, Year)`. +- `OpeningBalance` column type `decimal(18,2)`. +- Cascade delete → Entries. + +### Entry +- Index on `(AccountYearId, Date)`. +- `Amount` column type `decimal(18,2)`. +- `Type` stored as int. +- Optional FK to TransferLink (`TransferLinkId`), restrict delete. + +### TransferLink +- Unique index on `SourceEntryId`. +- Unique index on `TargetEntryId`. +- Relationships to SourceEntry and TargetEntry with `DeleteBehavior.Restrict`. + +## Notes +- Use separate `IEntityTypeConfiguration` classes in `Persistence/Configurations/`. +- Do not auto-generate migrations; the initial migration will be created manually later. diff --git a/.github/prompts/04-application-services.prompt.md b/.github/prompts/04-application-services.prompt.md new file mode 100644 index 0000000..ee3dd30 --- /dev/null +++ b/.github/prompts/04-application-services.prompt.md @@ -0,0 +1,57 @@ +--- +description: "Define and implement application service interfaces and implementations for accounts, years, entries, transfers, balance queries, and file operations" +agent: "agent" +--- + +# Step 4: Application Services + +## Interfaces (in `src/Duempelkas.App/Services/`) + +### IAccountService +- `Task> GetAllAccountsAsync()` — returns each account with its current total balance across all years. +- `Task CreateAccountAsync(string name)` +- `Task RenameAccountAsync(int accountId, string newName)` +- `Task DeleteAccountAsync(int accountId)` + +### IAccountYearService +- `Task> GetYearsForAccountAsync(int accountId)` +- `Task CreateYearAsync(int accountId, int year, decimal openingBalance)` +- `Task SuggestCarryoverAsync(int accountId, int year)` — calculates prior year's closing balance as suggestion. +- `Task UpdateOpeningBalanceAsync(int accountYearId, decimal openingBalance)` + +### IEntryService +- `Task> GetEntriesAsync(int accountYearId)` +- `Task CreateEntryAsync(int accountYearId, EntryType type, DateTime date, string title, decimal amount)` +- `Task CreateTransferAsync(int sourceAccountYearId, int targetAccountYearId, DateTime date, string title, decimal amount)` — atomically creates two entries + TransferLink. +- `Task DeleteEntryAsync(int entryId)` — if linked transfer, deletes both sides atomically. +- `Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount)` + +### IBalanceQueryService +- `Task GetAccountTotalBalanceAsync(int accountId)` — OpeningBalance + sum(Income) - sum(Expense) across all years. +- `Task GetYearlySummaryAsync(int accountYearId)` — returns opening balance, total income, total expense, yearly movement, closing balance. + +### IPdfStatementService +- `Task GenerateYearlyStatementAsync(int accountYearId)` + +### IFileSaveService +- `Task SaveFileAsync(byte[] content, string suggestedFileName)` — abstracts native file-save dialog. + +## DTOs (in `src/Duempelkas.App/Services/Models/`) +- `AccountSummaryDto` { Id, Name, TotalBalance, CreatedUtc } +- `AccountYearDto` { Id, AccountId, Year, OpeningBalance } +- `EntryDto` { Id, AccountYearId, Type, Date, Title, Amount, IsTransfer, TransferLinkId, LinkedAccountName } +- `YearlySummaryDto` { OpeningBalance, TotalIncome, TotalExpense, YearlyMovement, ClosingBalance } + +## Implementations (in `src/Duempelkas.Infrastructure/Services/`) +- Implement each interface using `FinanceDbContext`. +- All balance calculations use the single formula: `OpeningBalance + sum(Income) - sum(Expense)`. +- Transfer creation must be wrapped in a transaction. +- Entry deletion must check for TransferLink and delete both sides if linked. + +## DI Registration +- Create `src/Duempelkas.Infrastructure/DependencyInjection.cs` with `AddInfrastructure(this IServiceCollection, string connectionString)` extension method that registers DbContext and all services. + +## Conventions +- File-scoped namespaces. +- Async/await throughout. +- No business logic in entities; all in services. diff --git a/.github/prompts/05-ui-shell-and-pages.prompt.md b/.github/prompts/05-ui-shell-and-pages.prompt.md new file mode 100644 index 0000000..81084cb --- /dev/null +++ b/.github/prompts/05-ui-shell-and-pages.prompt.md @@ -0,0 +1,72 @@ +--- +description: "Build the Blazor Hybrid UI shell, dashboard page, account detail page, entry table, dialogs, and responsive styling" +agent: "agent" +--- + +# Step 5: UI Shell & Pages + +Build all Blazor components in `src/Duempelkas.App/`. + +## Page: Dashboard (`Pages/Dashboard.razor`) +- Route: `/` +- Displays all accounts as responsive Bootstrap cards using `AccountCardList` and `AccountCard` components. +- Each card shows account name and formatted total balance (Euro). +- A "Create Account" button opens `AddAccountDialog`. +- Clicking a card navigates to `/accounts/{id}`. + +## Page: Account Detail (`Pages/Accounts/AccountDetail.razor`) +- Route: `/accounts/{AccountId:int}` +- **Header area**: Account name, total balance (including carryover), year selector dropdown, "Add Year" button, "Export PDF" button. +- **Main area**: Chronological entry table (oldest to newest) for the selected year. +- "Add Entry" and "Add Transfer" buttons in a toolbar above the table. +- Show yearly summary at the bottom: Opening balance, Year movement, Closing balance. + +## Components (in `Components/Accounts/`) + +### AccountCardList.razor +- Renders a responsive grid of `AccountCard` components. + +### AccountCard.razor / AccountCard.razor.css +- Card displaying account name and total balance. +- Click handler for navigation. + +### AccountHeader.razor +- Displays account name, total balance, year selector, action buttons. + +### YearSelector.razor +- Dropdown to select the active year, fires `EventCallback` on change. + +### EntryTable.razor / EntryTable.razor.css +- Table with columns: Date, Title, Type badge, Amount. +- Rows sorted oldest → newest. +- Transfer rows get a distinct visual indicator (e.g., link icon, blue highlight). + +### EntryRow.razor +- Single table row. Badge color: green for Income, red for Expense, blue for Transfer-linked. + +### ExportButton.razor +- Calls `IPdfStatementService` then `IFileSaveService` to save the PDF. + +## Dialogs (in `Components/Dialogs/`) + +### AddAccountDialog.razor +- Modal with Name input. Calls `IAccountService.CreateAccountAsync`. + +### AddYearDialog.razor +- Modal with Year number input and Opening Balance input. Pre-fills suggested carryover. Calls `IAccountYearService.CreateYearAsync`. + +### AddEntryDialog.razor +- Modal with Type selector, Date, Title, Amount. Calls `IEntryService.CreateEntryAsync`. + +### AddTransferDialog.razor +- Modal with source/target account+year selectors, Date, Title, Amount. Calls `IEntryService.CreateTransferAsync`. + +## Styling +- Use Bootstrap 5 utilities for responsive layout. +- Custom CSS in `wwwroot/css/app.css` for type badges, card hover states, and table row highlights. +- Currency formatting: `amount.ToString("N2")` + " €" suffix. + +## Conventions +- Container/presentation split: pages load data, child components are presentational. +- Use `@inject` for services. +- Dialogs use a simple `bool IsVisible` pattern with backdrop overlay. diff --git a/.github/prompts/06-pdf-export.prompt.md b/.github/prompts/06-pdf-export.prompt.md new file mode 100644 index 0000000..85473b3 --- /dev/null +++ b/.github/prompts/06-pdf-export.prompt.md @@ -0,0 +1,43 @@ +--- +description: "Implement QuestPDF-based yearly account statement PDF generation with balance summaries" +agent: "agent" +--- + +# Step 6: PDF Export + +Implement `PdfStatementService` in `src/Duempelkas.Infrastructure/Services/`. + +## PDF Document Structure + +Generate one PDF per account/year with the following sections: + +### Header +- Account name (bold, large) +- Year (e.g., "Statement 2026") +- Generation date + +### Opening Balance Section +- "Carryover from previous year: X.XX €" + +### Ledger Table +- Columns: Date, Title, Type, Amount +- Rows sorted chronologically (oldest → newest) +- Type column shows "Income", "Expense", or "Transfer" (for linked entries) +- Transfer rows should include a note like "→ TargetAccountName" or "← SourceAccountName" +- Amount formatted as "1.234,56 €" (German locale) + +### Summary Footer +1. "Carryover from previous year: X.XX €" +2. "Current year result: X.XX €" (sum of incomes minus sum of expenses) +3. "Final balance: X.XX €" (carryover + current year result) + +## Implementation Details +- Use `QuestPDF.Fluent` API with `Document.Create(...)`. +- Set `QuestPDF.Settings.License = LicenseType.Community` in startup. +- Reuse `IBalanceQueryService.GetYearlySummaryAsync` and `IEntryService.GetEntriesAsync` to get the data — do NOT recalculate balances independently. +- Return `byte[]` from `GenerateYearlyStatementAsync`. +- Use A4 page size, reasonable margins, and professional typography. + +## Conventions +- Keep the PDF layout in a single service method or split into private helper methods. +- Use `CultureInfo("de-DE")` for number/currency formatting. diff --git a/.github/prompts/07-tests-and-validation.prompt.md b/.github/prompts/07-tests-and-validation.prompt.md new file mode 100644 index 0000000..36fe55a --- /dev/null +++ b/.github/prompts/07-tests-and-validation.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Create xUnit tests for transfer consistency, balance calculations, carryover logic, and PDF export totals" +agent: "agent" +--- + +# Step 7: Tests & Validation + +Create tests in `tests/Duempelkas.Tests/`. + +## Test 1: Transfer Consistency (`TransferServiceTests.cs`) + +### Scenario: CreateTransfer_CreatesLinkedExpenseAndIncome +Given: +- Account A and Account B exist +- Both have AccountYear for 2026 with OpeningBalance = 0 + +When: +- A transfer of 100.00 EUR is created from A/2026 to B/2026 with title "Test Transfer" + +Then: +- Exactly 2 entries are created in the database +- Entry on A is `EntryType.Expense`, amount = 100.00 +- Entry on B is `EntryType.Income`, amount = 100.00 +- Both entries share the same date and title +- Exactly 1 `TransferLink` exists linking the two entries +- Account A total balance = -100.00 +- Account B total balance = +100.00 + +### Scenario: DeleteTransfer_RemovesBothSides +Given: +- A transfer exists between A and B + +When: +- The source entry is deleted via `IEntryService.DeleteEntryAsync` + +Then: +- Both entries are removed +- The TransferLink is removed +- Account A and B balances return to 0 + +## Test 2: Yearly Statement Calculation (`YearlyStatementCalculationTests.cs`) + +### Scenario: YearlySummary_CalculatesCorrectTotals +Given: +- Account with Year 2026, OpeningBalance = 500.00 +- Entries: Income 800.00, Income 400.00, Expense 200.00, Expense 250.00 + +Then: +- TotalIncome = 1200.00 +- TotalExpense = 450.00 +- YearlyMovement = 750.00 +- ClosingBalance = 1250.00 + +### Scenario: YearlySummary_IncludesTransfersCorrectly +Given: +- Account A, Year 2026, OpeningBalance = 500.00 +- Income entry: 1000.00 +- Transfer out to Account B: 300.00 (creates Expense entry on A) + +Then: +- A's TotalIncome = 1000.00 +- A's TotalExpense = 300.00 +- A's YearlyMovement = 700.00 +- A's ClosingBalance = 1200.00 + +## Test Setup +- Use EF Core `InMemory` or SQLite in-memory (`Data Source=:memory:`) for test database. +- Wire up real service implementations against the test database. +- Use `FluentAssertions` for readable assertions. +- Arrange/Act/Assert pattern throughout. + +## Manual Verification Checklist (as code comments) +1. Launch on Windows — app window opens with dashboard +2. Launch on Linux — app window opens (requires WebKit2GTK) +3. Create account → appears on dashboard with 0 balance +4. Add year 2026 with carryover 500 → year appears in selector +5. Add income/expense entries → table populates, balance updates +6. Create transfer between accounts → both sides appear with transfer badge +7. Export PDF → file saves, totals match on-screen values +8. Delete transfer → both sides removed, balances correct