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:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Purrse.Core.Enums;
|
||||
|
||||
public enum AccountType
|
||||
{
|
||||
Checking,
|
||||
Savings,
|
||||
CreditCard,
|
||||
AutoLoan,
|
||||
PersonalLoan,
|
||||
Mortgage,
|
||||
LineOfCredit,
|
||||
Retirement401k,
|
||||
IRA,
|
||||
Brokerage,
|
||||
Cash
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Purrse.Core.Enums;
|
||||
|
||||
public enum CategoryType
|
||||
{
|
||||
Income,
|
||||
Expense,
|
||||
Transfer
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Purrse.Core.Enums;
|
||||
|
||||
public enum Frequency
|
||||
{
|
||||
Daily,
|
||||
Weekly,
|
||||
BiWeekly,
|
||||
SemiMonthly,
|
||||
Monthly,
|
||||
Quarterly,
|
||||
SemiAnnually,
|
||||
Annually
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user