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,33 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class AccountConfiguration : IEntityTypeConfiguration<Account>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
builder.ToTable("accounts");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Name).HasMaxLength(200).IsRequired();
|
||||
builder.Property(a => a.Institution).HasMaxLength(200);
|
||||
builder.Property(a => a.AccountNumber).HasMaxLength(50);
|
||||
builder.Property(a => a.Balance).HasPrecision(18, 2);
|
||||
builder.Property(a => a.CreditLimit).HasPrecision(18, 2);
|
||||
builder.Property(a => a.InterestRate).HasPrecision(8, 4);
|
||||
builder.Property(a => a.Notes).HasMaxLength(1000);
|
||||
|
||||
builder.HasOne(a => a.User)
|
||||
.WithMany(u => u.Accounts)
|
||||
.HasForeignKey(a => a.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(a => a.LoanDetail)
|
||||
.WithOne(l => l.Account)
|
||||
.HasForeignKey<LoanDetail>(l => l.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(a => a.UserId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class AmortizationEntryConfiguration : IEntityTypeConfiguration<AmortizationEntry>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AmortizationEntry> builder)
|
||||
{
|
||||
builder.ToTable("amortization_entries");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.PaymentAmount).HasPrecision(18, 2);
|
||||
builder.Property(a => a.PrincipalAmount).HasPrecision(18, 2);
|
||||
builder.Property(a => a.InterestAmount).HasPrecision(18, 2);
|
||||
builder.Property(a => a.EscrowAmount).HasPrecision(18, 2);
|
||||
builder.Property(a => a.RemainingBalance).HasPrecision(18, 2);
|
||||
|
||||
builder.HasOne(a => a.LoanDetail)
|
||||
.WithMany(l => l.AmortizationEntries)
|
||||
.HasForeignKey(a => a.LoanDetailId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class BudgetConfiguration : IEntityTypeConfiguration<Budget>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Budget> builder)
|
||||
{
|
||||
builder.ToTable("budgets");
|
||||
builder.HasKey(b => b.Id);
|
||||
builder.Property(b => b.Notes).HasMaxLength(500);
|
||||
|
||||
builder.HasOne(b => b.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(b => new { b.UserId, b.Year, b.Month }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class BudgetItemConfiguration : IEntityTypeConfiguration<BudgetItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BudgetItem> builder)
|
||||
{
|
||||
builder.ToTable("budget_items");
|
||||
builder.HasKey(bi => bi.Id);
|
||||
builder.Property(bi => bi.BudgetedAmount).HasPrecision(18, 2);
|
||||
|
||||
builder.HasOne(bi => bi.Budget)
|
||||
.WithMany(b => b.Items)
|
||||
.HasForeignKey(bi => bi.BudgetId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(bi => bi.Category)
|
||||
.WithMany(c => c.BudgetItems)
|
||||
.HasForeignKey(bi => bi.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Category> builder)
|
||||
{
|
||||
builder.ToTable("categories");
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Name).HasMaxLength(200).IsRequired();
|
||||
|
||||
builder.HasOne(c => c.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(c => c.Parent)
|
||||
.WithMany(c => c.Children)
|
||||
.HasForeignKey(c => c.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(c => new { c.UserId, c.Name, c.ParentId }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class ImportBatchConfiguration : IEntityTypeConfiguration<ImportBatch>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ImportBatch> builder)
|
||||
{
|
||||
builder.ToTable("import_batches");
|
||||
builder.HasKey(b => b.Id);
|
||||
builder.Property(b => b.FileName).HasMaxLength(500).IsRequired();
|
||||
builder.Property(b => b.FileType).HasMaxLength(50).IsRequired();
|
||||
|
||||
builder.HasOne(b => b.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(b => b.Account)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class InvestmentHoldingConfiguration : IEntityTypeConfiguration<InvestmentHolding>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<InvestmentHolding> builder)
|
||||
{
|
||||
builder.ToTable("investment_holdings");
|
||||
builder.HasKey(h => h.Id);
|
||||
builder.Property(h => h.Shares).HasPrecision(18, 6);
|
||||
builder.Property(h => h.CostBasis).HasPrecision(18, 2);
|
||||
|
||||
builder.HasOne(h => h.Account)
|
||||
.WithMany(a => a.InvestmentHoldings)
|
||||
.HasForeignKey(h => h.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(h => h.Security)
|
||||
.WithMany(s => s.Holdings)
|
||||
.HasForeignKey(h => h.SecurityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class SecurityConfiguration : IEntityTypeConfiguration<Security>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Security> builder)
|
||||
{
|
||||
builder.ToTable("securities");
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Symbol).HasMaxLength(20).IsRequired();
|
||||
builder.Property(s => s.Name).HasMaxLength(200).IsRequired();
|
||||
builder.Property(s => s.SecurityType).HasMaxLength(50);
|
||||
|
||||
builder.HasIndex(s => s.Symbol).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class SecurityPriceConfiguration : IEntityTypeConfiguration<SecurityPrice>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SecurityPrice> builder)
|
||||
{
|
||||
builder.ToTable("security_prices");
|
||||
builder.HasKey(p => p.Id);
|
||||
builder.Property(p => p.Price).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(p => p.Security)
|
||||
.WithMany(s => s.Prices)
|
||||
.HasForeignKey(p => p.SecurityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(p => new { p.SecurityId, p.Date }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class LoanDetailConfiguration : IEntityTypeConfiguration<LoanDetail>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<LoanDetail> builder)
|
||||
{
|
||||
builder.ToTable("loan_details");
|
||||
builder.HasKey(l => l.Id);
|
||||
builder.Property(l => l.OriginalBalance).HasPrecision(18, 2);
|
||||
builder.Property(l => l.InterestRate).HasPrecision(8, 4);
|
||||
builder.Property(l => l.MonthlyPayment).HasPrecision(18, 2);
|
||||
builder.Property(l => l.EscrowAmount).HasPrecision(18, 2);
|
||||
builder.Property(l => l.ExtraPayment).HasPrecision(18, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class PayeeAliasConfiguration : IEntityTypeConfiguration<PayeeAlias>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PayeeAlias> builder)
|
||||
{
|
||||
builder.ToTable("payee_aliases");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Alias).HasMaxLength(300).IsRequired();
|
||||
|
||||
builder.HasOne(a => a.Payee)
|
||||
.WithMany(p => p.Aliases)
|
||||
.HasForeignKey(a => a.PayeeId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(a => a.Alias);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class PayeeConfiguration : IEntityTypeConfiguration<Payee>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Payee> builder)
|
||||
{
|
||||
builder.ToTable("payees");
|
||||
builder.HasKey(p => p.Id);
|
||||
builder.Property(p => p.Name).HasMaxLength(300).IsRequired();
|
||||
|
||||
builder.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(p => p.DefaultCategory)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.DefaultCategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasIndex(p => new { p.UserId, p.Name }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class PluginRegistrationConfiguration : IEntityTypeConfiguration<PluginRegistration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PluginRegistration> builder)
|
||||
{
|
||||
builder.ToTable("plugin_registrations");
|
||||
builder.HasKey(p => p.Id);
|
||||
builder.Property(p => p.PluginId).HasMaxLength(200).IsRequired();
|
||||
builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
|
||||
builder.Property(p => p.Version).HasMaxLength(50).IsRequired();
|
||||
builder.Property(p => p.EntryAssembly).HasMaxLength(500);
|
||||
builder.Property(p => p.EntryType).HasMaxLength(500);
|
||||
|
||||
builder.HasIndex(p => p.PluginId).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginConfigurationEntityConfiguration : IEntityTypeConfiguration<Purrse.Core.Models.PluginConfiguration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Purrse.Core.Models.PluginConfiguration> builder)
|
||||
{
|
||||
builder.ToTable("plugin_configurations");
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Key).HasMaxLength(200).IsRequired();
|
||||
builder.Property(c => c.Value).HasMaxLength(2000);
|
||||
|
||||
builder.HasOne(c => c.PluginRegistration)
|
||||
.WithMany(p => p.Configurations)
|
||||
.HasForeignKey(c => c.PluginRegistrationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(c => new { c.PluginRegistrationId, c.Key }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class ReconciliationConfiguration : IEntityTypeConfiguration<Reconciliation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Reconciliation> builder)
|
||||
{
|
||||
builder.ToTable("reconciliations");
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.StatementBalance).HasPrecision(18, 2);
|
||||
|
||||
builder.HasOne(r => r.Account)
|
||||
.WithMany(a => a.Reconciliations)
|
||||
.HasForeignKey(r => r.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class ScheduledTransactionConfiguration : IEntityTypeConfiguration<ScheduledTransaction>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ScheduledTransaction> builder)
|
||||
{
|
||||
builder.ToTable("scheduled_transactions");
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Amount).HasPrecision(18, 2);
|
||||
builder.Property(s => s.PayeeName).HasMaxLength(500);
|
||||
builder.Property(s => s.Memo).HasMaxLength(1000);
|
||||
|
||||
builder.HasOne(s => s.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(s => s.Account)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(s => s.Payee)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.PayeeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasOne(s => s.Category)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.CategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class TransactionConfiguration : IEntityTypeConfiguration<Transaction>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Transaction> builder)
|
||||
{
|
||||
builder.ToTable("transactions");
|
||||
builder.HasKey(t => t.Id);
|
||||
builder.Property(t => t.Amount).HasPrecision(18, 2);
|
||||
builder.Property(t => t.PayeeName).HasMaxLength(500);
|
||||
builder.Property(t => t.Memo).HasMaxLength(1000);
|
||||
builder.Property(t => t.ReferenceNumber).HasMaxLength(100);
|
||||
builder.Property(t => t.CheckNumber).HasMaxLength(20);
|
||||
builder.Property(t => t.FitId).HasMaxLength(255);
|
||||
builder.Property(t => t.Fingerprint).HasMaxLength(64);
|
||||
|
||||
builder.HasOne(t => t.Account)
|
||||
.WithMany(a => a.Transactions)
|
||||
.HasForeignKey(t => t.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(t => t.Payee)
|
||||
.WithMany(p => p.Transactions)
|
||||
.HasForeignKey(t => t.PayeeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasOne(t => t.Category)
|
||||
.WithMany(c => c.Transactions)
|
||||
.HasForeignKey(t => t.CategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasOne(t => t.TransferTransaction)
|
||||
.WithOne()
|
||||
.HasForeignKey<Transaction>(t => t.TransferTransactionId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasOne(t => t.ImportBatch)
|
||||
.WithMany(b => b.Transactions)
|
||||
.HasForeignKey(t => t.ImportBatchId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasOne(t => t.Reconciliation)
|
||||
.WithMany(r => r.Transactions)
|
||||
.HasForeignKey(t => t.ReconciliationId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
builder.HasIndex(t => t.AccountId);
|
||||
builder.HasIndex(t => t.Date);
|
||||
builder.HasIndex(t => t.Fingerprint);
|
||||
builder.HasIndex(t => t.FitId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class TransactionSplitConfiguration : IEntityTypeConfiguration<TransactionSplit>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TransactionSplit> builder)
|
||||
{
|
||||
builder.ToTable("transaction_splits");
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Amount).HasPrecision(18, 2);
|
||||
builder.Property(s => s.Memo).HasMaxLength(500);
|
||||
|
||||
builder.HasOne(s => s.Transaction)
|
||||
.WithMany(t => t.Splits)
|
||||
.HasForeignKey(s => s.TransactionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(s => s.Category)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.CategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Configurations;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.ToTable("users");
|
||||
builder.HasKey(u => u.Id);
|
||||
builder.Property(u => u.Username).HasMaxLength(100).IsRequired();
|
||||
builder.Property(u => u.Email).HasMaxLength(255).IsRequired();
|
||||
builder.Property(u => u.PasswordHash).HasMaxLength(255).IsRequired();
|
||||
builder.HasIndex(u => u.Username).IsUnique();
|
||||
builder.HasIndex(u => u.Email).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Purrse.Core\Purrse.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data;
|
||||
|
||||
public class PurrseDbContext : DbContext
|
||||
{
|
||||
public PurrseDbContext(DbContextOptions<PurrseDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Account> Accounts => Set<Account>();
|
||||
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||
public DbSet<TransactionSplit> TransactionSplits => Set<TransactionSplit>();
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
public DbSet<Payee> Payees => Set<Payee>();
|
||||
public DbSet<PayeeAlias> PayeeAliases => Set<PayeeAlias>();
|
||||
public DbSet<LoanDetail> LoanDetails => Set<LoanDetail>();
|
||||
public DbSet<AmortizationEntry> AmortizationEntries => Set<AmortizationEntry>();
|
||||
public DbSet<Budget> Budgets => Set<Budget>();
|
||||
public DbSet<BudgetItem> BudgetItems => Set<BudgetItem>();
|
||||
public DbSet<ScheduledTransaction> ScheduledTransactions => Set<ScheduledTransaction>();
|
||||
public DbSet<ImportBatch> ImportBatches => Set<ImportBatch>();
|
||||
public DbSet<InvestmentHolding> InvestmentHoldings => Set<InvestmentHolding>();
|
||||
public DbSet<Security> Securities => Set<Security>();
|
||||
public DbSet<SecurityPrice> SecurityPrices => Set<SecurityPrice>();
|
||||
public DbSet<Reconciliation> Reconciliations => Set<Reconciliation>();
|
||||
public DbSet<PluginRegistration> PluginRegistrations => Set<PluginRegistration>();
|
||||
public DbSet<PluginConfiguration> PluginConfigurations => Set<PluginConfiguration>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PurrseDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Purrse.Core.Enums;
|
||||
using Purrse.Core.Models;
|
||||
|
||||
namespace Purrse.Data.Seeds;
|
||||
|
||||
public static class CategorySeeder
|
||||
{
|
||||
public static List<Category> GetDefaultCategories(Guid userId)
|
||||
{
|
||||
var categories = new List<Category>();
|
||||
int sort = 0;
|
||||
|
||||
// Income categories
|
||||
AddCategory(categories, userId, "Income", CategoryType.Income, null, ref sort, true,
|
||||
new[] { "Salary", "Bonus", "Interest Income", "Dividend Income", "Rental Income", "Freelance Income", "Gift Received", "Refund", "Other Income" });
|
||||
|
||||
// Expense categories
|
||||
AddCategory(categories, userId, "Housing", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Mortgage Payment", "Rent", "Property Tax", "Home Insurance", "HOA Fees", "Home Maintenance", "Home Improvement" });
|
||||
|
||||
AddCategory(categories, userId, "Utilities", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Electric", "Gas", "Water & Sewer", "Trash", "Internet", "Phone", "Cable/Streaming" });
|
||||
|
||||
AddCategory(categories, userId, "Food", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Groceries", "Restaurants", "Fast Food", "Coffee Shops" });
|
||||
|
||||
AddCategory(categories, userId, "Transportation", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Auto Payment", "Auto Insurance", "Fuel", "Parking", "Public Transit", "Auto Maintenance", "Registration & Fees", "Tolls" });
|
||||
|
||||
AddCategory(categories, userId, "Healthcare", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Health Insurance", "Doctor", "Dentist", "Pharmacy", "Vision", "Hospital" });
|
||||
|
||||
AddCategory(categories, userId, "Personal", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Clothing", "Personal Care", "Hair & Beauty", "Gym & Fitness", "Education", "Books", "Subscriptions" });
|
||||
|
||||
AddCategory(categories, userId, "Entertainment", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Movies & Shows", "Music", "Games", "Hobbies", "Sports", "Vacations" });
|
||||
|
||||
AddCategory(categories, userId, "Financial", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Bank Fees", "Credit Card Interest", "Loan Interest", "Late Fees", "ATM Fees" });
|
||||
|
||||
AddCategory(categories, userId, "Insurance", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Life Insurance", "Disability Insurance", "Umbrella Insurance" });
|
||||
|
||||
AddCategory(categories, userId, "Taxes", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Federal Tax", "State Tax", "Local Tax", "Social Security", "Medicare" });
|
||||
|
||||
AddCategory(categories, userId, "Gifts & Donations", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Charity", "Gifts Given", "Religious" });
|
||||
|
||||
AddCategory(categories, userId, "Pets", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Pet Food", "Veterinary", "Pet Supplies" });
|
||||
|
||||
AddCategory(categories, userId, "Children", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Child Care", "School Expenses", "Allowance", "Activities" });
|
||||
|
||||
AddCategory(categories, userId, "Miscellaneous", CategoryType.Expense, null, ref sort, true,
|
||||
new[] { "Other Expense", "Cash & ATM" });
|
||||
|
||||
// Transfer categories
|
||||
AddCategory(categories, userId, "Transfer", CategoryType.Transfer, null, ref sort, true,
|
||||
new[] { "Account Transfer", "Credit Card Payment", "Loan Payment", "Investment Contribution" });
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static void AddCategory(List<Category> categories, Guid userId, string parentName,
|
||||
CategoryType type, Guid? grandParentId, ref int sort, bool isSystem, string[]? children)
|
||||
{
|
||||
var parentId = Guid.NewGuid();
|
||||
categories.Add(new Category
|
||||
{
|
||||
Id = parentId,
|
||||
UserId = userId,
|
||||
Name = parentName,
|
||||
Type = type,
|
||||
ParentId = grandParentId,
|
||||
SortOrder = sort++,
|
||||
IsSystem = isSystem,
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
if (children == null) return;
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
categories.Add(new Category
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = child,
|
||||
Type = type,
|
||||
ParentId = parentId,
|
||||
SortOrder = sort++,
|
||||
IsSystem = isSystem,
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user