diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index 3a5a8a8..d0d86ba 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -7,6 +7,7 @@ export const useAuthStore = defineStore('auth', () => { const token = ref(null) const refreshTokenValue = ref(null) const username = ref(null) + const displayName = ref(null) const expiresAt = ref(null) const isAuthenticated = computed(() => !!token.value) @@ -15,6 +16,7 @@ export const useAuthStore = defineStore('auth', () => { token.value = localStorage.getItem('purrse_token') refreshTokenValue.value = localStorage.getItem('purrse_refresh_token') username.value = localStorage.getItem('purrse_username') + displayName.value = localStorage.getItem('purrse_display_name') expiresAt.value = localStorage.getItem('purrse_expires_at') } @@ -22,10 +24,16 @@ export const useAuthStore = defineStore('auth', () => { token.value = auth.token refreshTokenValue.value = auth.refreshToken username.value = auth.username + displayName.value = auth.displayName expiresAt.value = auth.expiresAt localStorage.setItem('purrse_token', auth.token) localStorage.setItem('purrse_refresh_token', auth.refreshToken) localStorage.setItem('purrse_username', auth.username) + if (auth.displayName) { + localStorage.setItem('purrse_display_name', auth.displayName) + } else { + localStorage.removeItem('purrse_display_name') + } localStorage.setItem('purrse_expires_at', auth.expiresAt) } @@ -45,16 +53,27 @@ export const useAuthStore = defineStore('auth', () => { setAuth(data) } + function setDisplayName(name: string | null) { + displayName.value = name + if (name) { + localStorage.setItem('purrse_display_name', name) + } else { + localStorage.removeItem('purrse_display_name') + } + } + function logout() { token.value = null refreshTokenValue.value = null username.value = null + displayName.value = null expiresAt.value = null localStorage.removeItem('purrse_token') localStorage.removeItem('purrse_refresh_token') localStorage.removeItem('purrse_username') + localStorage.removeItem('purrse_display_name') localStorage.removeItem('purrse_expires_at') } - return { token, username, expiresAt, isAuthenticated, initialize, login, register, refreshToken, logout } + return { token, username, displayName, expiresAt, isAuthenticated, initialize, login, register, refreshToken, setDisplayName, logout } }) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 1a6e473..60c2dd1 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -3,6 +3,25 @@ export interface AuthResponse { refreshToken: string expiresAt: string username: string + displayName: string | null +} + +export interface UserProfile { + username: string + displayName: string | null + email: string + createdAt: string + lastLoginAt: string | null +} + +export interface UpdateProfileRequest { + displayName?: string | null + email?: string +} + +export interface ChangePasswordRequest { + currentPassword: string + newPassword: string } export interface Account { diff --git a/frontend/src/views/settings/SettingsView.vue b/frontend/src/views/settings/SettingsView.vue index 5c116d9..d96a3fc 100644 --- a/frontend/src/views/settings/SettingsView.vue +++ b/frontend/src/views/settings/SettingsView.vue @@ -10,206 +10,456 @@ - - Profile + + + Profile Information + + + Save + + + + Change Password + + + + + + Change Password + + + + + + + Account Info + +
+ Member since: {{ profile ? new Date(profile.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) : '-' }} +
+
+ Last login: {{ profile?.lastLoginAt ? new Date(profile.lastLoginAt).toLocaleString() : 'Never' }} +
+
+
+ + + + Danger Zone + +

Permanently delete your account and all associated data. This action cannot be undone.

+ Delete Account +
+
+ + + + + Delete Account + + Are you sure you want to permanently delete your account? All your data including accounts, transactions, and settings will be removed. This cannot be undone. + + + + Cancel + Delete + + + + + + + {{ profileSuccessMessage }} + + + {{ profileErrorMessage }} +
- - - - mdi-key - Plaid API Credentials - Configured - Not configured - - -

- Enter your Plaid developer credentials. Get them from your Plaid dashboard. -

- - - - - - - - - - - - - Save - - - -
-
-
- - Connect via Plaid - - - Connect via SimpleFIN - -
- - - No bank connections yet. Configure your API credentials above, then click a button to connect your bank. - - - - - {{ conn.provider === 'Plaid' ? 'mdi-bank' : 'mdi-link-variant' }} - {{ conn.institutionName }} - - {{ conn.provider }} - + + + + mdi-link-variant + SimpleFIN + {{ simpleFinConnections.length }} - - {{ conn.isActive ? 'Active' : 'Inactive' }} - + Connect + + + Sync All + + + Re-sync All + -
- Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }} - {{ conn.lastSyncError }} -
- - - - - External Account - Type - Balance - Purrse Account - Enabled - Last Synced - - - - - {{ la.externalAccountName }} - {{ la.externalAccountType }} - {{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }} - - - - - - - {{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }} - - - - -
+
- - - Sync Now - - - Re-sync - - - {{ conn.isActive ? 'Disable' : 'Enable' }} - - - Delete -
+ + + No SimpleFIN connections yet. Click Connect to add one. + + + + + {{ conn.institutionName }} + + + {{ conn.isActive ? 'Active' : 'Inactive' }} + + + {{ conn.isActive ? 'Disable' : 'Enable' }} + + + + + +
+ Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }} + + Syncing... + + {{ conn.lastSyncError }} +
+ + + + + External Account + Type + Balance + Purrse Account + Enabled + Last Synced + + + + + {{ la.externalAccountName }} + {{ la.externalAccountType }} + {{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }} + + + + + + + {{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }} + + + +
+
+ + + + + + + mdi-bank + Plaid + {{ plaidConnections.length }} + + + Connect + + + Sync All + + + + + +
+
+ mdi-key + API Credentials + Configured + Not configured +
+

+ Enter your Plaid developer credentials. Get them from your Plaid dashboard. +

+ + + + + + + + + + + + + Save + + + +
+ + + + + No Plaid connections yet. Configure your API credentials above, then click Connect. + + + + + {{ conn.institutionName }} + + + {{ conn.isActive ? 'Active' : 'Inactive' }} + + + {{ conn.isActive ? 'Disable' : 'Enable' }} + + + + + +
+ Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }} + + Syncing... + + {{ conn.lastSyncError }} +
+ + + + + External Account + Type + Balance + Purrse Account + Enabled + Last Synced + + + + + {{ la.externalAccountName }} + {{ la.externalAccountType }} + {{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }} + + + + + + + {{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }} + + + + +
+ +
+
+
@@ -525,16 +775,101 @@ diff --git a/src/Purrse.Api/Controllers/AuthController.cs b/src/Purrse.Api/Controllers/AuthController.cs index 745e30d..a7427cf 100644 --- a/src/Purrse.Api/Controllers/AuthController.cs +++ b/src/Purrse.Api/Controllers/AuthController.cs @@ -43,4 +43,40 @@ public class AuthController : ControllerBase await _authService.RevokeRefreshTokenAsync(userId); return NoContent(); } + + [Authorize] + [HttpGet("profile")] + public async Task> GetProfile() + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + var profile = await _authService.GetProfileAsync(userId); + return Ok(profile); + } + + [Authorize] + [HttpPut("profile")] + public async Task> UpdateProfile(UpdateProfileRequest request) + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + var profile = await _authService.UpdateProfileAsync(userId, request); + return Ok(profile); + } + + [Authorize] + [HttpPost("change-password")] + public async Task ChangePassword(ChangePasswordRequest request) + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + await _authService.ChangePasswordAsync(userId, request); + return NoContent(); + } + + [Authorize] + [HttpDelete("account")] + public async Task DeleteAccount() + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + await _authService.DeleteAccountAsync(userId); + return NoContent(); + } } diff --git a/src/Purrse.Api/Services/AuthService.cs b/src/Purrse.Api/Services/AuthService.cs index 3d348b7..b3c26c8 100644 --- a/src/Purrse.Api/Services/AuthService.cs +++ b/src/Purrse.Api/Services/AuthService.cs @@ -82,6 +82,54 @@ public class AuthService : IAuthService } } + public async Task GetProfileAsync(Guid userId) + { + var user = await _db.Users.FindAsync(userId) + ?? throw new KeyNotFoundException("User not found"); + + return new UserProfileResponse(user.Username, user.DisplayName, user.Email, user.CreatedAt, user.LastLoginAt); + } + + public async Task UpdateProfileAsync(Guid userId, UpdateProfileRequest request) + { + var user = await _db.Users.FindAsync(userId) + ?? throw new KeyNotFoundException("User not found"); + + if (request.DisplayName != null) + user.DisplayName = request.DisplayName; + + if (request.Email != null && request.Email != user.Email) + { + if (await _db.Users.AnyAsync(u => u.Email == request.Email && u.Id != userId)) + throw new InvalidOperationException("Email already in use"); + user.Email = request.Email; + } + + await _db.SaveChangesAsync(); + return new UserProfileResponse(user.Username, user.DisplayName, user.Email, user.CreatedAt, user.LastLoginAt); + } + + public async Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request) + { + var user = await _db.Users.FindAsync(userId) + ?? throw new KeyNotFoundException("User not found"); + + if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, user.PasswordHash)) + throw new InvalidOperationException("Current password is incorrect"); + + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); + await _db.SaveChangesAsync(); + } + + public async Task DeleteAccountAsync(Guid userId) + { + var user = await _db.Users.FindAsync(userId) + ?? throw new KeyNotFoundException("User not found"); + + _db.Users.Remove(user); + await _db.SaveChangesAsync(); + } + private AuthResponse GenerateAuthResponse(User user) { var expiresAt = DateTime.UtcNow.AddHours(12); @@ -92,7 +140,7 @@ public class AuthService : IAuthService user.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(30); _db.SaveChanges(); - return new AuthResponse(token, refreshToken, expiresAt, user.Username); + return new AuthResponse(token, refreshToken, expiresAt, user.Username, user.DisplayName); } private string GenerateJwtToken(User user, DateTime expiresAt) diff --git a/src/Purrse.Core/DTOs/AuthDtos.cs b/src/Purrse.Core/DTOs/AuthDtos.cs index 180793a..598ff6b 100644 --- a/src/Purrse.Core/DTOs/AuthDtos.cs +++ b/src/Purrse.Core/DTOs/AuthDtos.cs @@ -2,5 +2,16 @@ 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 AuthResponse(string Token, string RefreshToken, DateTime ExpiresAt, string Username, string? DisplayName); public record RefreshRequest(string RefreshToken); + +public record UserProfileResponse( + string Username, + string? DisplayName, + string Email, + DateTime CreatedAt, + DateTime? LastLoginAt); + +public record UpdateProfileRequest(string? DisplayName, string? Email); + +public record ChangePasswordRequest(string CurrentPassword, string NewPassword); diff --git a/src/Purrse.Core/Interfaces/Services/IAuthService.cs b/src/Purrse.Core/Interfaces/Services/IAuthService.cs index e8320ed..bcedf0e 100644 --- a/src/Purrse.Core/Interfaces/Services/IAuthService.cs +++ b/src/Purrse.Core/Interfaces/Services/IAuthService.cs @@ -8,4 +8,8 @@ public interface IAuthService Task LoginAsync(LoginRequest request); Task RefreshTokenAsync(string refreshToken); Task RevokeRefreshTokenAsync(Guid userId); + Task GetProfileAsync(Guid userId); + Task UpdateProfileAsync(Guid userId, UpdateProfileRequest request); + Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request); + Task DeleteAccountAsync(Guid userId); } diff --git a/src/Purrse.Core/Models/User.cs b/src/Purrse.Core/Models/User.cs index 26a7bbe..fab5fd3 100644 --- a/src/Purrse.Core/Models/User.cs +++ b/src/Purrse.Core/Models/User.cs @@ -4,6 +4,7 @@ public class User { public Guid Id { get; set; } public string Username { get; set; } = string.Empty; + public string? DisplayName { get; set; } public string Email { get; set; } = string.Empty; public string PasswordHash { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/src/Purrse.Data/Configurations/UserConfiguration.cs b/src/Purrse.Data/Configurations/UserConfiguration.cs index 9135989..907cfd0 100644 --- a/src/Purrse.Data/Configurations/UserConfiguration.cs +++ b/src/Purrse.Data/Configurations/UserConfiguration.cs @@ -11,6 +11,7 @@ public class UserConfiguration : IEntityTypeConfiguration builder.ToTable("users"); builder.HasKey(u => u.Id); builder.Property(u => u.Username).HasMaxLength(100).IsRequired(); + builder.Property(u => u.DisplayName).HasMaxLength(100); builder.Property(u => u.Email).HasMaxLength(255).IsRequired(); builder.Property(u => u.PasswordHash).HasMaxLength(255).IsRequired(); builder.HasIndex(u => u.Username).IsUnique(); diff --git a/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.Designer.cs b/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.Designer.cs new file mode 100644 index 0000000..c3d385b --- /dev/null +++ b/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.Designer.cs @@ -0,0 +1,1476 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Purrse.Data; + +#nullable disable + +namespace Purrse.Data.Migrations +{ + [DbContext(typeof(PurrseDbContext))] + [Migration("20260210033255_AddDisplayName")] + partial class AddDisplayName + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Purrse.Core.Models.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Balance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditLimit") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Institution") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("InterestRate") + .HasPrecision(8, 4) + .HasColumnType("numeric(8,4)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsClosed") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.AiCategorizationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConfidenceThreshold") + .HasColumnType("double precision"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OllamaUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SystemPrompt") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("TimeoutSeconds") + .HasColumnType("integer"); + + b.Property("UpdatePayeeDefaults") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPromptTemplate") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ai_categorization_settings", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.AmortizationEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EscrowAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("InterestAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("LoanDetailId") + .HasColumnType("uuid"); + + b.Property("PaymentAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PaymentNumber") + .HasColumnType("integer"); + + b.Property("PrincipalAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RemainingBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("LoanDetailId"); + + b.ToTable("amortization_entries", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.BankSyncSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedPlaidClientId") + .HasColumnType("text"); + + b.Property("EncryptedPlaidSecret") + .HasColumnType("text"); + + b.Property("PlaidEnvironment") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("bank_sync_settings", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Year", "Month") + .IsUnique(); + + b.ToTable("budgets", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("BudgetedAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.HasIndex("CategoryId"); + + b.ToTable("budget_items", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("UserId", "Name", "ParentId") + .IsUnique(); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("DuplicateCount") + .HasColumnType("integer"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ImportedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImportedCount") + .HasColumnType("integer"); + + b.Property("ParsedDataJson") + .HasColumnType("text"); + + b.Property("SkippedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("import_batches", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.InvestmentHolding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AsOfDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CostBasis") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SecurityId") + .HasColumnType("uuid"); + + b.Property("Shares") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("SecurityId"); + + b.ToTable("investment_holdings", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.LinkedAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalAccountId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExternalAccountMask") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExternalAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExternalAccountType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastKnownBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("LastSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SyncConnectionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("SyncConnectionId", "ExternalAccountId") + .IsUnique(); + + b.ToTable("linked_accounts", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("EscrowAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExtraPayment") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("InterestRate") + .HasPrecision(8, 4) + .HasColumnType("numeric(8,4)"); + + b.Property("MaturityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MonthlyPayment") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OriginalBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OriginationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TermMonths") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .IsUnique(); + + b.ToTable("loan_details", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Payee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultCategoryId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DefaultCategoryId"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("payees", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.PayeeAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alias") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("PayeeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Alias"); + + b.HasIndex("PayeeId"); + + b.ToTable("payee_aliases", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.PluginConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PluginRegistrationId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("PluginRegistrationId", "Key") + .IsUnique(); + + b.ToTable("plugin_configurations", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.PluginRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntryAssembly") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EntryType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InstalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PluginId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("PluginId") + .IsUnique(); + + b.ToTable("plugin_registrations", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompleted") + .HasColumnType("boolean"); + + b.Property("StatementBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("StatementDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("reconciliations", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.ScheduledTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AutoPost") + .HasColumnType("boolean"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Frequency") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Memo") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("NextDueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PayeeId") + .HasColumnType("uuid"); + + b.Property("PayeeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReminderDaysBefore") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("PayeeId"); + + b.HasIndex("UserId"); + + b.ToTable("scheduled_transactions", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Security", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SecurityType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Symbol") + .IsUnique(); + + b.ToTable("securities", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.SecurityPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Price") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SecurityId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SecurityId", "Date") + .IsUnique(); + + b.ToTable("security_prices", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.SyncConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedAccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstitutionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("InstitutionName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("PlaidItemId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("SyncIntervalMinutes") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlaidItemId"); + + b.HasIndex("UserId"); + + b.ToTable("sync_connections", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.SyncLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("LinkedAccountId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SyncConnectionId") + .HasColumnType("uuid"); + + b.Property("TransactionsFetched") + .HasColumnType("integer"); + + b.Property("TransactionsImported") + .HasColumnType("integer"); + + b.Property("TransactionsSkipped") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LinkedAccountId"); + + b.HasIndex("StartedAt"); + + b.HasIndex("SyncConnectionId"); + + b.ToTable("sync_logs", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CheckNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Fingerprint") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FitId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ImportBatchId") + .HasColumnType("uuid"); + + b.Property("IsVoid") + .HasColumnType("boolean"); + + b.Property("Memo") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PayeeId") + .HasColumnType("uuid"); + + b.Property("PayeeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReconciliationId") + .HasColumnType("uuid"); + + b.Property("ReferenceNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TransferTransactionId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Date"); + + b.HasIndex("Fingerprint"); + + b.HasIndex("FitId"); + + b.HasIndex("ImportBatchId"); + + b.HasIndex("PayeeId"); + + b.HasIndex("ReconciliationId"); + + b.HasIndex("TransferTransactionId") + .IsUnique(); + + b.ToTable("transactions", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.TransactionSplit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Memo") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TransactionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("TransactionId"); + + b.ToTable("transaction_splits", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RefreshToken") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Purrse.Core.Models.Account", b => + { + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.AiCategorizationSettings", b => + { + b.HasOne("Purrse.Core.Models.User", "User") + .WithOne("AiCategorizationSettings") + .HasForeignKey("Purrse.Core.Models.AiCategorizationSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.AmortizationEntry", b => + { + b.HasOne("Purrse.Core.Models.LoanDetail", "LoanDetail") + .WithMany("AmortizationEntries") + .HasForeignKey("LoanDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LoanDetail"); + }); + + modelBuilder.Entity("Purrse.Core.Models.BankSyncSettings", b => + { + b.HasOne("Purrse.Core.Models.User", "User") + .WithOne("BankSyncSettings") + .HasForeignKey("Purrse.Core.Models.BankSyncSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Budget", b => + { + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.BudgetItem", b => + { + b.HasOne("Purrse.Core.Models.Budget", "Budget") + .WithMany("Items") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Purrse.Core.Models.Category", "Category") + .WithMany("BudgetItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Category", b => + { + b.HasOne("Purrse.Core.Models.Category", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.InvestmentHolding", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany("InvestmentHoldings") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Purrse.Core.Models.Security", "Security") + .WithMany("Holdings") + .HasForeignKey("SecurityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Security"); + }); + + modelBuilder.Entity("Purrse.Core.Models.LinkedAccount", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany("LinkedAccounts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.SyncConnection", "SyncConnection") + .WithMany("LinkedAccounts") + .HasForeignKey("SyncConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("SyncConnection"); + }); + + modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithOne("LoanDetail") + .HasForeignKey("Purrse.Core.Models.LoanDetail", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Payee", b => + { + b.HasOne("Purrse.Core.Models.Category", "DefaultCategory") + .WithMany() + .HasForeignKey("DefaultCategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DefaultCategory"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.PayeeAlias", b => + { + b.HasOne("Purrse.Core.Models.Payee", "Payee") + .WithMany("Aliases") + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Payee"); + }); + + modelBuilder.Entity("Purrse.Core.Models.PluginConfiguration", b => + { + b.HasOne("Purrse.Core.Models.PluginRegistration", "PluginRegistration") + .WithMany("Configurations") + .HasForeignKey("PluginRegistrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PluginRegistration"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany("Reconciliations") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Purrse.Core.Models.ScheduledTransaction", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Purrse.Core.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.Payee", "Payee") + .WithMany() + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("Payee"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.SecurityPrice", b => + { + b.HasOne("Purrse.Core.Models.Security", "Security") + .WithMany("Prices") + .HasForeignKey("SecurityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Security"); + }); + + modelBuilder.Entity("Purrse.Core.Models.SyncConnection", b => + { + b.HasOne("Purrse.Core.Models.User", "User") + .WithMany("SyncConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Purrse.Core.Models.SyncLog", b => + { + b.HasOne("Purrse.Core.Models.LinkedAccount", "LinkedAccount") + .WithMany() + .HasForeignKey("LinkedAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.SyncConnection", "SyncConnection") + .WithMany("SyncLogs") + .HasForeignKey("SyncConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LinkedAccount"); + + b.Navigation("SyncConnection"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Transaction", b => + { + b.HasOne("Purrse.Core.Models.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Purrse.Core.Models.Category", "Category") + .WithMany("Transactions") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.ImportBatch", "ImportBatch") + .WithMany("Transactions") + .HasForeignKey("ImportBatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.Payee", "Payee") + .WithMany("Transactions") + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.Reconciliation", "Reconciliation") + .WithMany("Transactions") + .HasForeignKey("ReconciliationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.Transaction", "TransferTransaction") + .WithOne() + .HasForeignKey("Purrse.Core.Models.Transaction", "TransferTransactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("ImportBatch"); + + b.Navigation("Payee"); + + b.Navigation("Reconciliation"); + + b.Navigation("TransferTransaction"); + }); + + modelBuilder.Entity("Purrse.Core.Models.TransactionSplit", b => + { + b.HasOne("Purrse.Core.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Purrse.Core.Models.Transaction", "Transaction") + .WithMany("Splits") + .HasForeignKey("TransactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Account", b => + { + b.Navigation("InvestmentHoldings"); + + b.Navigation("LinkedAccounts"); + + b.Navigation("LoanDetail"); + + b.Navigation("Reconciliations"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Budget", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Category", b => + { + b.Navigation("BudgetItems"); + + b.Navigation("Children"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b => + { + b.Navigation("AmortizationEntries"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Payee", b => + { + b.Navigation("Aliases"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Purrse.Core.Models.PluginRegistration", b => + { + b.Navigation("Configurations"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Security", b => + { + b.Navigation("Holdings"); + + b.Navigation("Prices"); + }); + + modelBuilder.Entity("Purrse.Core.Models.SyncConnection", b => + { + b.Navigation("LinkedAccounts"); + + b.Navigation("SyncLogs"); + }); + + modelBuilder.Entity("Purrse.Core.Models.Transaction", b => + { + b.Navigation("Splits"); + }); + + modelBuilder.Entity("Purrse.Core.Models.User", b => + { + b.Navigation("Accounts"); + + b.Navigation("AiCategorizationSettings"); + + b.Navigation("BankSyncSettings"); + + b.Navigation("SyncConnections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.cs b/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.cs new file mode 100644 index 0000000..669d0d3 --- /dev/null +++ b/src/Purrse.Data/Migrations/20260210033255_AddDisplayName.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Purrse.Data.Migrations +{ + /// + public partial class AddDisplayName : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DisplayName", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DisplayName", + table: "users"); + } + } +} diff --git a/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs b/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs index 0810e39..9cc65bc 100644 --- a/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs +++ b/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs @@ -998,6 +998,10 @@ namespace Purrse.Data.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("Email") .IsRequired() .HasMaxLength(255)