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:
35
.github/prompts/01-solution-skeleton.prompt.md
vendored
Normal file
35
.github/prompts/01-solution-skeleton.prompt.md
vendored
Normal 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.
|
||||
63
.github/prompts/02-domain-model.prompt.md
vendored
Normal file
63
.github/prompts/02-domain-model.prompt.md
vendored
Normal 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.
|
||||
44
.github/prompts/03-dbcontext-mappings.prompt.md
vendored
Normal file
44
.github/prompts/03-dbcontext-mappings.prompt.md
vendored
Normal 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.
|
||||
57
.github/prompts/04-application-services.prompt.md
vendored
Normal file
57
.github/prompts/04-application-services.prompt.md
vendored
Normal 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.
|
||||
72
.github/prompts/05-ui-shell-and-pages.prompt.md
vendored
Normal file
72
.github/prompts/05-ui-shell-and-pages.prompt.md
vendored
Normal 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
43
.github/prompts/06-pdf-export.prompt.md
vendored
Normal 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.
|
||||
80
.github/prompts/07-tests-and-validation.prompt.md
vendored
Normal file
80
.github/prompts/07-tests-and-validation.prompt.md
vendored
Normal 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
|
||||
Reference in New Issue
Block a user