Add GitHub Copilot prompt files

- Step-by-step prompts for solution skeleton, domain model,
  DbContext mappings, application services, UI shell/pages,
  PDF export, and tests/validation
This commit is contained in:
2026-03-31 17:12:35 +02:00
parent 8b09173ccc
commit e589cc170a
7 changed files with 394 additions and 0 deletions

View File

@@ -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 `<App>`, 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 `<app>` 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.

View File

@@ -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<AccountYear> 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<Entry> 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.

View File

@@ -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<Account> Accounts`
- `DbSet<AccountYear> AccountYears`
- `DbSet<Entry> Entries`
- `DbSet<TransferLink> 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<T>` 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<T>` classes in `Persistence/Configurations/`.
- Do not auto-generate migrations; the initial migration will be created manually later.

View File

@@ -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<List<AccountSummaryDto>> GetAllAccountsAsync()` — returns each account with its current total balance across all years.
- `Task<AccountSummaryDto> CreateAccountAsync(string name)`
- `Task RenameAccountAsync(int accountId, string newName)`
- `Task DeleteAccountAsync(int accountId)`
### IAccountYearService
- `Task<List<AccountYearDto>> GetYearsForAccountAsync(int accountId)`
- `Task<AccountYearDto> CreateYearAsync(int accountId, int year, decimal openingBalance)`
- `Task<decimal> SuggestCarryoverAsync(int accountId, int year)` — calculates prior year's closing balance as suggestion.
- `Task UpdateOpeningBalanceAsync(int accountYearId, decimal openingBalance)`
### IEntryService
- `Task<List<EntryDto>> GetEntriesAsync(int accountYearId)`
- `Task<EntryDto> 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<decimal> GetAccountTotalBalanceAsync(int accountId)` — OpeningBalance + sum(Income) - sum(Expense) across all years.
- `Task<YearlySummaryDto> GetYearlySummaryAsync(int accountYearId)` — returns opening balance, total income, total expense, yearly movement, closing balance.
### IPdfStatementService
- `Task<byte[]> GenerateYearlyStatementAsync(int accountYearId)`
### IFileSaveService
- `Task<string?> 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.

View File

@@ -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<int>` 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.

43
.github/prompts/06-pdf-export.prompt.md vendored Normal file
View File

@@ -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.

View File

@@ -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