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
+53
View File
@@ -0,0 +1,53 @@
using Purrse.Core.Enums;
namespace Purrse.Core.DTOs;
public record CreateAccountRequest(
string Name,
AccountType Type,
string? Institution,
string? AccountNumber,
decimal Balance,
decimal? CreditLimit,
decimal? InterestRate,
string? Notes
);
public record UpdateAccountRequest(
string Name,
string? Institution,
string? AccountNumber,
decimal? CreditLimit,
decimal? InterestRate,
string? Notes,
bool IsActive,
int SortOrder
);
public record AccountResponse(
Guid Id,
string Name,
AccountType Type,
string? Institution,
string? AccountNumber,
decimal Balance,
decimal? CreditLimit,
decimal? InterestRate,
bool IsActive,
bool IsClosed,
string? Notes,
int SortOrder,
DateTime CreatedAt,
bool HasLoanDetail
);
public record AccountSummaryResponse(
Guid Id,
string Name,
AccountType Type,
string? Institution,
decimal Balance,
decimal? CreditLimit
);
public record BalanceHistoryEntry(DateTime Date, decimal Balance);
+6
View File
@@ -0,0 +1,6 @@
namespace Purrse.Core.DTOs;
public record RegisterRequest(string Username, string Email, string Password);
public record LoginRequest(string Username, string Password);
public record AuthResponse(string Token, string RefreshToken, DateTime ExpiresAt, string Username);
public record RefreshRequest(string RefreshToken);
+22
View File
@@ -0,0 +1,22 @@
namespace Purrse.Core.DTOs;
public record CreateBudgetRequest(int Year, int Month, string? Notes, List<BudgetItemRequest> Items);
public record UpdateBudgetRequest(string? Notes, List<BudgetItemRequest> Items);
public record BudgetItemRequest(Guid CategoryId, decimal BudgetedAmount);
public record BudgetResponse(
Guid Id,
int Year,
int Month,
string? Notes,
List<BudgetItemResponse> Items,
DateTime CreatedAt
);
public record BudgetItemResponse(
Guid Id,
Guid CategoryId,
string CategoryName,
decimal BudgetedAmount,
decimal ActualAmount
);
+27
View File
@@ -0,0 +1,27 @@
using Purrse.Core.Enums;
namespace Purrse.Core.DTOs;
public record CreateCategoryRequest(string Name, CategoryType Type, Guid? ParentId);
public record UpdateCategoryRequest(string Name, Guid? ParentId, int SortOrder, bool IsActive);
public record CategoryResponse(
Guid Id,
string Name,
CategoryType Type,
Guid? ParentId,
string? ParentName,
int SortOrder,
bool IsSystem,
bool IsActive
);
public record CategoryTreeResponse(
Guid Id,
string Name,
CategoryType Type,
int SortOrder,
bool IsSystem,
bool IsActive,
List<CategoryTreeResponse> Children
);
+27
View File
@@ -0,0 +1,27 @@
namespace Purrse.Core.DTOs;
public record DashboardResponse(
decimal NetWorth,
decimal TotalAssets,
decimal TotalLiabilities,
List<AccountSummaryResponse> Accounts,
List<TransactionResponse> RecentTransactions,
SpendingSummary SpendingSummary,
List<UpcomingBill> UpcomingBills
);
public record SpendingSummary(
decimal ThisMonth,
decimal LastMonth,
decimal ChangePercent,
List<CategorySpending> TopCategories
);
public record UpcomingBill(
Guid Id,
string PayeeName,
decimal Amount,
DateTime DueDate,
string AccountName,
bool AutoPost
);
+38
View File
@@ -0,0 +1,38 @@
using Purrse.Core.Enums;
namespace Purrse.Core.DTOs;
public record ImportUploadResponse(
Guid BatchId,
int TotalTransactions,
int NewTransactions,
int PossibleDuplicates,
List<ImportedTransactionPreview> Previews
);
public record ImportedTransactionPreview(
int Index,
DateTime Date,
decimal Amount,
string? PayeeName,
string? Memo,
string? FitId,
DuplicateMatch? DuplicateMatch
);
public record DuplicateMatch(
Guid ExistingTransactionId,
double Confidence,
string MatchReason
);
public record ResolveDuplicatesRequest(List<DuplicateResolution> Resolutions);
public record DuplicateResolution(int Index, DuplicateAction Action);
public enum DuplicateAction
{
Import,
Skip,
Merge
}
+22
View File
@@ -0,0 +1,22 @@
namespace Purrse.Core.DTOs;
public record InvestmentHoldingResponse(
Guid Id,
string Symbol,
string SecurityName,
decimal Shares,
decimal CostBasis,
decimal CurrentPrice,
decimal MarketValue,
decimal GainLoss,
decimal GainLossPercent,
DateTime AsOfDate
);
public record InvestmentPerformanceResponse(
decimal TotalMarketValue,
decimal TotalCostBasis,
decimal TotalGainLoss,
decimal TotalGainLossPercent,
List<InvestmentHoldingResponse> Holdings
);
+50
View File
@@ -0,0 +1,50 @@
namespace Purrse.Core.DTOs;
public record CreateLoanDetailRequest(
decimal OriginalBalance,
decimal InterestRate,
int TermMonths,
decimal MonthlyPayment,
DateTime OriginationDate,
DateTime MaturityDate,
decimal? EscrowAmount,
decimal? ExtraPayment
);
public record LoanDetailResponse(
Guid Id,
Guid AccountId,
string AccountName,
decimal OriginalBalance,
decimal CurrentBalance,
decimal InterestRate,
int TermMonths,
decimal MonthlyPayment,
DateTime OriginationDate,
DateTime MaturityDate,
decimal? EscrowAmount,
decimal? ExtraPayment,
List<AmortizationEntryResponse> AmortizationSchedule
);
public record AmortizationEntryResponse(
int PaymentNumber,
DateTime PaymentDate,
decimal PaymentAmount,
decimal PrincipalAmount,
decimal InterestAmount,
decimal? EscrowAmount,
decimal RemainingBalance
);
public record PayoffScenarioRequest(decimal? ExtraMonthlyPayment, decimal? LumpSumPayment, DateTime? LumpSumDate);
public record PayoffScenarioResponse(
DateTime OriginalPayoffDate,
DateTime NewPayoffDate,
decimal TotalInterestOriginal,
decimal TotalInterestNew,
decimal InterestSaved,
int MonthsSaved,
List<AmortizationEntryResponse> NewSchedule
);
+15
View File
@@ -0,0 +1,15 @@
namespace Purrse.Core.DTOs;
public record CreatePayeeRequest(string Name, Guid? DefaultCategoryId, List<string>? Aliases);
public record UpdatePayeeRequest(string Name, Guid? DefaultCategoryId, bool IsActive);
public record PayeeResponse(
Guid Id,
string Name,
Guid? DefaultCategoryId,
string? DefaultCategoryName,
bool IsActive,
List<string> Aliases
);
public record CreatePayeeAliasRequest(string Alias);
+14
View File
@@ -0,0 +1,14 @@
namespace Purrse.Core.DTOs;
public record PluginResponse(
Guid Id,
string PluginId,
string Name,
string Version,
bool IsEnabled,
DateTime InstalledAt,
List<string> Capabilities,
Dictionary<string, string> Configuration
);
public record PluginConfigurationRequest(Dictionary<string, string> Settings);
@@ -0,0 +1,14 @@
namespace Purrse.Core.DTOs;
public record StartReconciliationRequest(DateTime StatementDate, decimal StatementBalance);
public record ReconciliationResponse(
Guid Id,
Guid AccountId,
DateTime StatementDate,
decimal StatementBalance,
decimal ClearedBalance,
decimal Difference,
bool IsCompleted,
List<TransactionResponse> UnclearedTransactions
);
+32
View File
@@ -0,0 +1,32 @@
namespace Purrse.Core.DTOs;
public record SpendingByCategoryReport(
DateTime StartDate,
DateTime EndDate,
decimal TotalSpending,
List<CategorySpending> Categories
);
public record CategorySpending(string CategoryName, decimal Amount, decimal Percentage, string? ParentCategoryName);
public record IncomeVsExpenseReport(
DateTime StartDate,
DateTime EndDate,
List<MonthlyIncomeExpense> Months
);
public record MonthlyIncomeExpense(int Year, int Month, decimal Income, decimal Expenses, decimal Net);
public record NetWorthReport(List<NetWorthEntry> Entries);
public record NetWorthEntry(DateTime Date, decimal Assets, decimal Liabilities, decimal NetWorth);
public record CashFlowReport(
DateTime StartDate,
DateTime EndDate,
decimal TotalInflow,
decimal TotalOutflow,
decimal NetCashFlow,
List<CashFlowEntry> Entries
);
public record CashFlowEntry(DateTime Date, decimal Inflow, decimal Outflow, decimal Net);
@@ -0,0 +1,51 @@
using Purrse.Core.Enums;
namespace Purrse.Core.DTOs;
public record CreateScheduledTransactionRequest(
Guid AccountId,
decimal Amount,
string? PayeeName,
Guid? PayeeId,
Guid? CategoryId,
string? Memo,
TransactionType Type,
Frequency Frequency,
DateTime NextDueDate,
DateTime? EndDate,
bool AutoPost,
int ReminderDaysBefore
);
public record UpdateScheduledTransactionRequest(
decimal Amount,
string? PayeeName,
Guid? PayeeId,
Guid? CategoryId,
string? Memo,
TransactionType Type,
Frequency Frequency,
DateTime NextDueDate,
DateTime? EndDate,
bool AutoPost,
int ReminderDaysBefore,
bool IsActive
);
public record ScheduledTransactionResponse(
Guid Id,
Guid AccountId,
string AccountName,
decimal Amount,
string? PayeeName,
Guid? CategoryId,
string? CategoryName,
string? Memo,
TransactionType Type,
Frequency Frequency,
DateTime NextDueDate,
DateTime? EndDate,
bool AutoPost,
int ReminderDaysBefore,
bool IsActive
);
+81
View File
@@ -0,0 +1,81 @@
using Purrse.Core.Enums;
namespace Purrse.Core.DTOs;
public record CreateTransactionRequest(
Guid AccountId,
DateTime Date,
decimal Amount,
string? PayeeName,
Guid? PayeeId,
Guid? CategoryId,
string? Memo,
string? ReferenceNumber,
string? CheckNumber,
TransactionType Type,
List<SplitRequest>? Splits
);
public record UpdateTransactionRequest(
DateTime Date,
decimal Amount,
string? PayeeName,
Guid? PayeeId,
Guid? CategoryId,
string? Memo,
string? ReferenceNumber,
string? CheckNumber,
TransactionType Type,
TransactionStatus Status,
List<SplitRequest>? Splits
);
public record SplitRequest(Guid? CategoryId, decimal Amount, string? Memo);
public record TransactionResponse(
Guid Id,
Guid AccountId,
string? AccountName,
DateTime Date,
decimal Amount,
Guid? PayeeId,
string? PayeeName,
Guid? CategoryId,
string? CategoryName,
string? Memo,
string? ReferenceNumber,
string? CheckNumber,
TransactionType Type,
TransactionStatus Status,
bool IsVoid,
Guid? TransferTransactionId,
Guid? ImportBatchId,
List<SplitResponse>? Splits,
DateTime CreatedAt
);
public record SplitResponse(Guid Id, Guid? CategoryId, string? CategoryName, decimal Amount, string? Memo);
public record CreateTransferRequest(
Guid FromAccountId,
Guid ToAccountId,
DateTime Date,
decimal Amount,
string? Memo
);
public record TransactionSearchRequest(
Guid? AccountId,
DateTime? StartDate,
DateTime? EndDate,
decimal? MinAmount,
decimal? MaxAmount,
string? PayeeName,
Guid? CategoryId,
TransactionStatus? Status,
string? SearchText,
int Page = 1,
int PageSize = 50
);
public record PagedResult<T>(List<T> Items, int TotalCount, int Page, int PageSize, int TotalPages);
+16
View File
@@ -0,0 +1,16 @@
namespace Purrse.Core.Enums;
public enum AccountType
{
Checking,
Savings,
CreditCard,
AutoLoan,
PersonalLoan,
Mortgage,
LineOfCredit,
Retirement401k,
IRA,
Brokerage,
Cash
}
+8
View File
@@ -0,0 +1,8 @@
namespace Purrse.Core.Enums;
public enum CategoryType
{
Income,
Expense,
Transfer
}
+13
View File
@@ -0,0 +1,13 @@
namespace Purrse.Core.Enums;
public enum Frequency
{
Daily,
Weekly,
BiWeekly,
SemiMonthly,
Monthly,
Quarterly,
SemiAnnually,
Annually
}
+8
View File
@@ -0,0 +1,8 @@
namespace Purrse.Core.Enums;
public enum ImportStatus
{
Pending,
Confirmed,
Rejected
}
@@ -0,0 +1,8 @@
namespace Purrse.Core.Enums;
public enum TransactionStatus
{
Uncleared,
Cleared,
Reconciled
}
+8
View File
@@ -0,0 +1,8 @@
namespace Purrse.Core.Enums;
public enum TransactionType
{
Debit,
Credit,
Transfer
}
@@ -0,0 +1,15 @@
using F23.StringSimilarity;
namespace Purrse.Core.Helpers;
public static class StringSimilarityHelper
{
private static readonly JaroWinkler _jaroWinkler = new();
public static double Calculate(string s1, string s2)
{
if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2))
return 0;
return _jaroWinkler.Similarity(s1.ToUpperInvariant(), s2.ToUpperInvariant());
}
}
@@ -0,0 +1,14 @@
using System.Security.Cryptography;
using System.Text;
namespace Purrse.Core.Helpers;
public static class TransactionFingerprint
{
public static string Generate(Guid accountId, DateTime date, decimal amount, string? fitId)
{
var input = $"{accountId}|{date:yyyyMMdd}|{amount:F2}|{fitId ?? ""}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexStringLower(bytes);
}
}
@@ -0,0 +1,14 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IAccountService
{
Task<List<AccountResponse>> GetAllAsync(Guid userId);
Task<AccountResponse?> GetByIdAsync(Guid userId, Guid accountId);
Task<AccountResponse> CreateAsync(Guid userId, CreateAccountRequest request);
Task<AccountResponse> UpdateAsync(Guid userId, Guid accountId, UpdateAccountRequest request);
Task DeleteAsync(Guid userId, Guid accountId);
Task<List<BalanceHistoryEntry>> GetBalanceHistoryAsync(Guid userId, Guid accountId, DateTime startDate, DateTime endDate);
Task RecalculateBalanceAsync(Guid accountId);
}
@@ -0,0 +1,11 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IAuthService
{
Task<AuthResponse> RegisterAsync(RegisterRequest request);
Task<AuthResponse> LoginAsync(LoginRequest request);
Task<AuthResponse> RefreshTokenAsync(string refreshToken);
Task RevokeRefreshTokenAsync(Guid userId);
}
@@ -0,0 +1,12 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IBudgetService
{
Task<BudgetResponse?> GetByMonthAsync(Guid userId, int year, int month);
Task<BudgetResponse> CreateAsync(Guid userId, CreateBudgetRequest request);
Task<BudgetResponse> UpdateAsync(Guid userId, Guid budgetId, UpdateBudgetRequest request);
Task DeleteAsync(Guid userId, Guid budgetId);
Task<List<BudgetItemResponse>> GetActualsAsync(Guid userId, Guid budgetId);
}
@@ -0,0 +1,14 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface ICategoryService
{
Task<List<CategoryResponse>> GetAllAsync(Guid userId);
Task<List<CategoryTreeResponse>> GetTreeAsync(Guid userId);
Task<CategoryResponse?> GetByIdAsync(Guid userId, Guid categoryId);
Task<CategoryResponse> CreateAsync(Guid userId, CreateCategoryRequest request);
Task<CategoryResponse> UpdateAsync(Guid userId, Guid categoryId, UpdateCategoryRequest request);
Task DeleteAsync(Guid userId, Guid categoryId);
Task SeedDefaultCategoriesAsync(Guid userId);
}
@@ -0,0 +1,8 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IDashboardService
{
Task<DashboardResponse> GetDashboardAsync(Guid userId);
}
@@ -0,0 +1,10 @@
using Purrse.Core.DTOs;
using Purrse.Core.Models;
namespace Purrse.Core.Interfaces.Services;
public interface IDuplicateDetectionService
{
Task<DuplicateMatch?> FindDuplicateAsync(Guid accountId, DateTime date, decimal amount, string? payeeName, string? fitId, string? checkNumber);
double CalculateConfidence(Transaction existing, DateTime date, decimal amount, string? payeeName, string? fitId, string? checkNumber);
}
@@ -0,0 +1,22 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IImportService
{
Task<ImportUploadResponse> UploadAsync(Guid userId, Guid accountId, string fileName, Stream fileStream);
Task<ImportBatchResponse> ConfirmBatchAsync(Guid userId, Guid batchId);
Task ResolveDuplicatesAsync(Guid userId, Guid batchId, ResolveDuplicatesRequest request);
}
public record ImportBatchResponse(
Guid Id,
string FileName,
string FileType,
int TotalCount,
int ImportedCount,
int DuplicateCount,
int SkippedCount,
string Status,
DateTime ImportedAt
);
@@ -0,0 +1,12 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface ILoanService
{
Task<LoanDetailResponse?> GetLoanDetailAsync(Guid userId, Guid accountId);
Task<LoanDetailResponse> CreateLoanDetailAsync(Guid userId, Guid accountId, CreateLoanDetailRequest request);
Task<LoanDetailResponse> UpdateLoanDetailAsync(Guid userId, Guid accountId, CreateLoanDetailRequest request);
Task<List<AmortizationEntryResponse>> GetAmortizationScheduleAsync(Guid userId, Guid accountId);
Task<PayoffScenarioResponse> CalculatePayoffScenarioAsync(Guid userId, Guid accountId, PayoffScenarioRequest request);
}
@@ -0,0 +1,16 @@
using Purrse.Core.DTOs;
using Purrse.Core.Models;
namespace Purrse.Core.Interfaces.Services;
public interface IPayeeService
{
Task<List<PayeeResponse>> GetAllAsync(Guid userId);
Task<PayeeResponse?> GetByIdAsync(Guid userId, Guid payeeId);
Task<PayeeResponse> CreateAsync(Guid userId, CreatePayeeRequest request);
Task<PayeeResponse> UpdateAsync(Guid userId, Guid payeeId, UpdatePayeeRequest request);
Task DeleteAsync(Guid userId, Guid payeeId);
Task AddAliasAsync(Guid userId, Guid payeeId, string alias);
Task RemoveAliasAsync(Guid userId, Guid payeeId, Guid aliasId);
Task<Payee?> MatchPayeeAsync(Guid userId, string importedName);
}
@@ -0,0 +1,11 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IReportService
{
Task<SpendingByCategoryReport> GetSpendingByCategoryAsync(Guid userId, DateTime startDate, DateTime endDate, Guid? accountId = null);
Task<IncomeVsExpenseReport> GetIncomeVsExpenseAsync(Guid userId, DateTime startDate, DateTime endDate);
Task<NetWorthReport> GetNetWorthAsync(Guid userId, DateTime startDate, DateTime endDate);
Task<CashFlowReport> GetCashFlowAsync(Guid userId, DateTime startDate, DateTime endDate);
}
@@ -0,0 +1,15 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface IScheduledTransactionService
{
Task<List<ScheduledTransactionResponse>> GetAllAsync(Guid userId);
Task<ScheduledTransactionResponse?> GetByIdAsync(Guid userId, Guid id);
Task<ScheduledTransactionResponse> CreateAsync(Guid userId, CreateScheduledTransactionRequest request);
Task<ScheduledTransactionResponse> UpdateAsync(Guid userId, Guid id, UpdateScheduledTransactionRequest request);
Task DeleteAsync(Guid userId, Guid id);
Task SkipAsync(Guid userId, Guid id);
Task<TransactionResponse> PostNowAsync(Guid userId, Guid id);
Task ProcessDueTransactionsAsync();
}
@@ -0,0 +1,15 @@
using Purrse.Core.DTOs;
namespace Purrse.Core.Interfaces.Services;
public interface ITransactionService
{
Task<PagedResult<TransactionResponse>> GetByAccountAsync(Guid userId, Guid accountId, int page, int pageSize);
Task<TransactionResponse?> GetByIdAsync(Guid userId, Guid transactionId);
Task<TransactionResponse> CreateAsync(Guid userId, CreateTransactionRequest request);
Task<TransactionResponse> UpdateAsync(Guid userId, Guid transactionId, UpdateTransactionRequest request);
Task DeleteAsync(Guid userId, Guid transactionId);
Task<PagedResult<TransactionResponse>> SearchAsync(Guid userId, TransactionSearchRequest request);
Task<(TransactionResponse From, TransactionResponse To)> CreateTransferAsync(Guid userId, CreateTransferRequest request);
Task VoidAsync(Guid userId, Guid transactionId);
}
+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>();
}
+13
View File
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="F23.StringSimilarity" Version="4.1.0" />
</ItemGroup>
</Project>