Private
Public Access
1
0

Initial commit: Purrse personal finance app

Self-hosted, plugin-extensible personal finance manager built with
ASP.NET Core 9.0, Vue 3 + Vuetify 3, and PostgreSQL 17.

Backend (8 .NET projects):
- Core: 19 domain models, 6 enums, 14 DTOs, 12 service interfaces
- Data: EF Core DbContext, 16 entity configurations, category seeder
- API: 14 controllers, 15 services, JWT auth, SignalR, middleware
- Plugins: Abstractions + OFX/CSV/QIF file parsers
- Tests: 28 xUnit tests (fingerprinting, duplicate detection, parsers)

Frontend (Vue 3 + Vuetify 3 + TypeScript):
- 13 views, Pinia stores, Axios API services with JWT interceptors
- Dashboard, accounts, transactions, categories, imports, and more

Deployment:
- Docker Compose (PostgreSQL 17 + .NET API + nginx frontend)
- Auto-migration on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 08:56:46 -05:00
commit 6520ebf221
169 changed files with 7180 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class Account
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public AccountType Type { get; set; }
public string? Institution { get; set; }
public string? AccountNumber { get; set; }
public decimal Balance { get; set; }
public decimal? CreditLimit { get; set; }
public decimal? InterestRate { get; set; }
public bool IsActive { get; set; } = true;
public bool IsClosed { get; set; }
public string? Notes { get; set; }
public int SortOrder { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
public LoanDetail? LoanDetail { get; set; }
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public ICollection<InvestmentHolding> InvestmentHoldings { get; set; } = new List<InvestmentHolding>();
public ICollection<Reconciliation> Reconciliations { get; set; } = new List<Reconciliation>();
}
@@ -0,0 +1,16 @@
namespace Purrse.Core.Models;
public class AmortizationEntry
{
public Guid Id { get; set; }
public Guid LoanDetailId { get; set; }
public int PaymentNumber { get; set; }
public DateTime PaymentDate { get; set; }
public decimal PaymentAmount { get; set; }
public decimal PrincipalAmount { get; set; }
public decimal InterestAmount { get; set; }
public decimal? EscrowAmount { get; set; }
public decimal RemainingBalance { get; set; }
public LoanDetail LoanDetail { get; set; } = null!;
}
+14
View File
@@ -0,0 +1,14 @@
namespace Purrse.Core.Models;
public class Budget
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public int Year { get; set; }
public int Month { get; set; }
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
public ICollection<BudgetItem> Items { get; set; } = new List<BudgetItem>();
}
+12
View File
@@ -0,0 +1,12 @@
namespace Purrse.Core.Models;
public class BudgetItem
{
public Guid Id { get; set; }
public Guid BudgetId { get; set; }
public Guid CategoryId { get; set; }
public decimal BudgetedAmount { get; set; }
public Budget Budget { get; set; } = null!;
public Category Category { get; set; } = null!;
}
+21
View File
@@ -0,0 +1,21 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class Category
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public CategoryType Type { get; set; }
public Guid? ParentId { get; set; }
public int SortOrder { get; set; }
public bool IsSystem { get; set; }
public bool IsActive { get; set; } = true;
public User User { get; set; } = null!;
public Category? Parent { get; set; }
public ICollection<Category> Children { get; set; } = new List<Category>();
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public ICollection<BudgetItem> BudgetItems { get; set; } = new List<BudgetItem>();
}
+22
View File
@@ -0,0 +1,22 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class ImportBatch
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid AccountId { get; set; }
public string FileName { get; set; } = string.Empty;
public string FileType { get; set; } = string.Empty;
public int TotalCount { get; set; }
public int ImportedCount { get; set; }
public int DuplicateCount { get; set; }
public int SkippedCount { get; set; }
public ImportStatus Status { get; set; } = ImportStatus.Pending;
public DateTime ImportedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
public Account Account { get; set; } = null!;
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
}
@@ -0,0 +1,14 @@
namespace Purrse.Core.Models;
public class InvestmentHolding
{
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public Guid SecurityId { get; set; }
public decimal Shares { get; set; }
public decimal CostBasis { get; set; }
public DateTime AsOfDate { get; set; }
public Account Account { get; set; } = null!;
public Security Security { get; set; } = null!;
}
+18
View File
@@ -0,0 +1,18 @@
namespace Purrse.Core.Models;
public class LoanDetail
{
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public decimal OriginalBalance { get; set; }
public decimal InterestRate { get; set; }
public int TermMonths { get; set; }
public decimal MonthlyPayment { get; set; }
public DateTime OriginationDate { get; set; }
public DateTime MaturityDate { get; set; }
public decimal? EscrowAmount { get; set; }
public decimal? ExtraPayment { get; set; }
public Account Account { get; set; } = null!;
public ICollection<AmortizationEntry> AmortizationEntries { get; set; } = new List<AmortizationEntry>();
}
+15
View File
@@ -0,0 +1,15 @@
namespace Purrse.Core.Models;
public class Payee
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public Guid? DefaultCategoryId { get; set; }
public bool IsActive { get; set; } = true;
public User User { get; set; } = null!;
public Category? DefaultCategory { get; set; }
public ICollection<PayeeAlias> Aliases { get; set; } = new List<PayeeAlias>();
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
}
+10
View File
@@ -0,0 +1,10 @@
namespace Purrse.Core.Models;
public class PayeeAlias
{
public Guid Id { get; set; }
public Guid PayeeId { get; set; }
public string Alias { get; set; } = string.Empty;
public Payee Payee { get; set; } = null!;
}
@@ -0,0 +1,11 @@
namespace Purrse.Core.Models;
public class PluginConfiguration
{
public Guid Id { get; set; }
public Guid PluginRegistrationId { get; set; }
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public PluginRegistration PluginRegistration { get; set; } = null!;
}
@@ -0,0 +1,15 @@
namespace Purrse.Core.Models;
public class PluginRegistration
{
public Guid Id { get; set; }
public string PluginId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string EntryAssembly { get; set; } = string.Empty;
public string EntryType { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public DateTime InstalledAt { get; set; } = DateTime.UtcNow;
public ICollection<PluginConfiguration> Configurations { get; set; } = new List<PluginConfiguration>();
}
+15
View File
@@ -0,0 +1,15 @@
namespace Purrse.Core.Models;
public class Reconciliation
{
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public DateTime StatementDate { get; set; }
public decimal StatementBalance { get; set; }
public bool IsCompleted { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public Account Account { get; set; } = null!;
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
}
@@ -0,0 +1,28 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class ScheduledTransaction
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid AccountId { get; set; }
public decimal Amount { get; set; }
public string? PayeeName { get; set; }
public Guid? PayeeId { get; set; }
public Guid? CategoryId { get; set; }
public string? Memo { get; set; }
public TransactionType Type { get; set; }
public Frequency Frequency { get; set; }
public DateTime NextDueDate { get; set; }
public DateTime? EndDate { get; set; }
public bool AutoPost { get; set; }
public int ReminderDaysBefore { get; set; } = 3;
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
public Account Account { get; set; } = null!;
public Payee? Payee { get; set; }
public Category? Category { get; set; }
}
+12
View File
@@ -0,0 +1,12 @@
namespace Purrse.Core.Models;
public class Security
{
public Guid Id { get; set; }
public string Symbol { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? SecurityType { get; set; }
public ICollection<InvestmentHolding> Holdings { get; set; } = new List<InvestmentHolding>();
public ICollection<SecurityPrice> Prices { get; set; } = new List<SecurityPrice>();
}
+11
View File
@@ -0,0 +1,11 @@
namespace Purrse.Core.Models;
public class SecurityPrice
{
public Guid Id { get; set; }
public Guid SecurityId { get; set; }
public DateTime Date { get; set; }
public decimal Price { get; set; }
public Security Security { get; set; } = null!;
}
+35
View File
@@ -0,0 +1,35 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class Transaction
{
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public Guid? PayeeId { get; set; }
public string? PayeeName { get; set; }
public Guid? CategoryId { get; set; }
public string? Memo { get; set; }
public string? ReferenceNumber { get; set; }
public string? CheckNumber { get; set; }
public TransactionType Type { get; set; }
public TransactionStatus Status { get; set; } = TransactionStatus.Uncleared;
public string? FitId { get; set; }
public string? Fingerprint { get; set; }
public Guid? TransferTransactionId { get; set; }
public Guid? ImportBatchId { get; set; }
public Guid? ReconciliationId { get; set; }
public bool IsVoid { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public Account Account { get; set; } = null!;
public Payee? Payee { get; set; }
public Category? Category { get; set; }
public Transaction? TransferTransaction { get; set; }
public ImportBatch? ImportBatch { get; set; }
public Reconciliation? Reconciliation { get; set; }
public ICollection<TransactionSplit> Splits { get; set; } = new List<TransactionSplit>();
}
@@ -0,0 +1,13 @@
namespace Purrse.Core.Models;
public class TransactionSplit
{
public Guid Id { get; set; }
public Guid TransactionId { get; set; }
public Guid? CategoryId { get; set; }
public decimal Amount { get; set; }
public string? Memo { get; set; }
public Transaction Transaction { get; set; } = null!;
public Category? Category { get; set; }
}
+15
View File
@@ -0,0 +1,15 @@
namespace Purrse.Core.Models;
public class User
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; set; }
public string? RefreshToken { get; set; }
public DateTime? RefreshTokenExpiresAt { get; set; }
public ICollection<Account> Accounts { get; set; } = new List<Account>();
}