-
mdi-robot-outline
+
mdi-cat
Ask me about your finances
(null)
const conversationsLoaded = ref(false)
+const chatBotName = ref('Purrse AI')
let _idCounter = 0
function tempId(): string {
@@ -214,6 +216,18 @@ function togglePanel() {
panelOpen.value = !panelOpen.value
if (panelOpen.value && !conversationsLoaded.value) {
loadConversations()
+ loadChatBotName()
+ }
+}
+
+async function loadChatBotName() {
+ try {
+ const { data } = await aiCategorizationApi.getSettings()
+ if (data.chatBotName) {
+ chatBotName.value = data.chatBotName
+ }
+ } catch {
+ // Default name is fine
}
}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index e342a3e..45fcbf1 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -521,6 +521,8 @@ export interface AiCategorizationSettings {
updatePayeeDefaults: boolean
systemPrompt: string | null
userPromptTemplate: string | null
+ chatContextSize: number
+ chatBotName: string | null
}
export interface OllamaConnectionTestResult {
diff --git a/frontend/src/views/settings/SettingsView.vue b/frontend/src/views/settings/SettingsView.vue
index d96a3fc..c588958 100644
--- a/frontend/src/views/settings/SettingsView.vue
+++ b/frontend/src/views/settings/SettingsView.vue
@@ -762,6 +762,37 @@
+
+ AI Chat Settings
+
+ Context Size: {{ aiSettings.chatContextSize.toLocaleString() }} tokens
+
+
+ Estimated VRAM: ~{{ Math.round((aiSettings.chatContextSize / 1024) * 75) }} MB — Increase if the AI loses context in long chats. Decrease if Ollama runs out of memory.
+
+
+
+
+
+
AI categorization settings saved
@@ -1305,7 +1336,9 @@ const aiSettings = reactive({
timeoutSeconds: 30,
updatePayeeDefaults: true,
systemPrompt: null,
- userPromptTemplate: null
+ userPromptTemplate: null,
+ chatContextSize: 16384,
+ chatBotName: null
})
const aiSaving = ref(false)
const aiTesting = ref(false)
diff --git a/src/Purrse.Api/Services/AiCategorizationService.cs b/src/Purrse.Api/Services/AiCategorizationService.cs
index 15578cc..6d55804 100644
--- a/src/Purrse.Api/Services/AiCategorizationService.cs
+++ b/src/Purrse.Api/Services/AiCategorizationService.cs
@@ -67,13 +67,14 @@ public class AiCategorizationService : IAiCategorizationService
if (settings == null)
{
return new AiCategorizationSettingsResponse(
- false, "http://localhost:11434", "llama3.1:8b", 0.7, 30, true, null, null);
+ false, "http://localhost:11434", "llama3.1:8b", 0.7, 30, true, null, null, 16384, null);
}
return new AiCategorizationSettingsResponse(
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
- settings.SystemPrompt, settings.UserPromptTemplate);
+ settings.SystemPrompt, settings.UserPromptTemplate,
+ settings.ChatContextSize, settings.ChatBotName);
}
public async Task UpdateSettingsAsync(Guid userId, UpdateAiCategorizationSettingsRequest request)
@@ -98,6 +99,8 @@ public class AiCategorizationService : IAiCategorizationService
settings.UpdatePayeeDefaults = request.UpdatePayeeDefaults;
settings.SystemPrompt = string.IsNullOrWhiteSpace(request.SystemPrompt) ? null : request.SystemPrompt;
settings.UserPromptTemplate = string.IsNullOrWhiteSpace(request.UserPromptTemplate) ? null : request.UserPromptTemplate;
+ settings.ChatContextSize = request.ChatContextSize;
+ settings.ChatBotName = string.IsNullOrWhiteSpace(request.ChatBotName) ? null : request.ChatBotName;
settings.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
@@ -105,7 +108,8 @@ public class AiCategorizationService : IAiCategorizationService
return new AiCategorizationSettingsResponse(
settings.IsEnabled, settings.OllamaUrl, settings.ModelName,
settings.ConfidenceThreshold, settings.TimeoutSeconds, settings.UpdatePayeeDefaults,
- settings.SystemPrompt, settings.UserPromptTemplate);
+ settings.SystemPrompt, settings.UserPromptTemplate,
+ settings.ChatContextSize, settings.ChatBotName);
}
public async Task TestConnectionAsync(Guid userId)
diff --git a/src/Purrse.Api/Services/ChatService.cs b/src/Purrse.Api/Services/ChatService.cs
index 57e87ae..8feeafe 100644
--- a/src/Purrse.Api/Services/ChatService.cs
+++ b/src/Purrse.Api/Services/ChatService.cs
@@ -113,6 +113,8 @@ public class ChatService : IChatService
var settings = await _db.AiCategorizationSettings.FirstOrDefaultAsync(s => s.UserId == userId);
var ollamaUrl = settings?.OllamaUrl ?? "http://localhost:11434";
var modelName = settings?.ModelName ?? "llama3.1:8b";
+ var chatContextSize = settings?.ChatContextSize ?? 16384;
+ var chatBotName = settings?.ChatBotName;
// Save user message
var userMessage = new ChatMessage
@@ -130,10 +132,15 @@ public class ChatService : IChatService
var ollamaMessages = new List();
// System prompt
+ var systemPrompt = string.Format(SystemPromptTemplate, DateTime.UtcNow.ToString("yyyy-MM-dd"));
+ if (!string.IsNullOrWhiteSpace(chatBotName))
+ {
+ systemPrompt += $"\nYour name is {chatBotName}.";
+ }
ollamaMessages.Add(new OllamaToolMessage
{
Role = "system",
- Content = string.Format(SystemPromptTemplate, DateTime.UtcNow.ToString("yyyy-MM-dd"))
+ Content = systemPrompt
});
// Last 20 conversation messages
@@ -199,7 +206,7 @@ public class ChatService : IChatService
Messages = ollamaMessages,
Stream = false,
Tools = tools,
- Options = new OllamaRequestOptions { NumCtx = 16384 }
+ Options = new OllamaRequestOptions { NumCtx = chatContextSize }
};
var response = await CallOllamaAsync(ollamaUrl, ollamaRequest);
@@ -272,7 +279,7 @@ public class ChatService : IChatService
Messages = ollamaMessages,
Stream = false,
Tools = null, // drop tools so the model focuses on the page data
- Options = new OllamaRequestOptions { NumCtx = 16384 }
+ Options = new OllamaRequestOptions { NumCtx = chatContextSize }
};
var retryResponse = await CallOllamaAsync(ollamaUrl, retryRequest);
rawContent = retryResponse?.Message?.Content;
diff --git a/src/Purrse.Core/DTOs/AiCategorizationDtos.cs b/src/Purrse.Core/DTOs/AiCategorizationDtos.cs
index 47c141c..4a45702 100644
--- a/src/Purrse.Core/DTOs/AiCategorizationDtos.cs
+++ b/src/Purrse.Core/DTOs/AiCategorizationDtos.cs
@@ -11,7 +11,9 @@ public record AiCategorizationSettingsResponse(
int TimeoutSeconds,
bool UpdatePayeeDefaults,
string? SystemPrompt,
- string? UserPromptTemplate);
+ string? UserPromptTemplate,
+ int ChatContextSize,
+ string? ChatBotName);
public record UpdateAiCategorizationSettingsRequest(
bool IsEnabled,
@@ -21,7 +23,9 @@ public record UpdateAiCategorizationSettingsRequest(
int TimeoutSeconds,
bool UpdatePayeeDefaults,
string? SystemPrompt,
- string? UserPromptTemplate);
+ string? UserPromptTemplate,
+ int ChatContextSize,
+ string? ChatBotName);
// Connection test
public record OllamaConnectionTestResponse(
diff --git a/src/Purrse.Core/Models/AiCategorizationSettings.cs b/src/Purrse.Core/Models/AiCategorizationSettings.cs
index f466d02..5f74ec0 100644
--- a/src/Purrse.Core/Models/AiCategorizationSettings.cs
+++ b/src/Purrse.Core/Models/AiCategorizationSettings.cs
@@ -12,6 +12,8 @@ public class AiCategorizationSettings
public bool UpdatePayeeDefaults { get; set; } = true;
public string? SystemPrompt { get; set; }
public string? UserPromptTemplate { get; set; }
+ public int ChatContextSize { get; set; } = 16384;
+ public string? ChatBotName { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
diff --git a/src/Purrse.Data/Configurations/AiCategorizationSettingsConfiguration.cs b/src/Purrse.Data/Configurations/AiCategorizationSettingsConfiguration.cs
index 8b202f6..7753170 100644
--- a/src/Purrse.Data/Configurations/AiCategorizationSettingsConfiguration.cs
+++ b/src/Purrse.Data/Configurations/AiCategorizationSettingsConfiguration.cs
@@ -14,6 +14,7 @@ public class AiCategorizationSettingsConfiguration : IEntityTypeConfiguration s.ModelName).HasMaxLength(100);
builder.Property(s => s.SystemPrompt).HasMaxLength(4000);
builder.Property(s => s.UserPromptTemplate).HasMaxLength(4000);
+ builder.Property(s => s.ChatBotName).HasMaxLength(100);
builder.HasOne(s => s.User)
.WithOne(u => u.AiCategorizationSettings)
diff --git a/src/Purrse.Data/Migrations/20260213192005_AddChatSettings.Designer.cs b/src/Purrse.Data/Migrations/20260213192005_AddChatSettings.Designer.cs
new file mode 100644
index 0000000..06bbb5f
--- /dev/null
+++ b/src/Purrse.Data/Migrations/20260213192005_AddChatSettings.Designer.cs
@@ -0,0 +1,1570 @@
+//
+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("20260213192005_AddChatSettings")]
+ partial class AddChatSettings
+ {
+ ///
+ 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("ChatBotName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ChatContextSize")
+ .HasColumnType("integer");
+
+ 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.ChatConversation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Title")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("chat_conversations", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ChatMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ConversationId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("ToolCallsJson")
+ .HasColumnType("text");
+
+ b.Property("ToolResultJson")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ConversationId");
+
+ b.ToTable("chat_messages", (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.ChatConversation", b =>
+ {
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ChatMessage", b =>
+ {
+ b.HasOne("Purrse.Core.Models.ChatConversation", "Conversation")
+ .WithMany("Messages")
+ .HasForeignKey("ConversationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Conversation");
+ });
+
+ 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.ChatConversation", b =>
+ {
+ b.Navigation("Messages");
+ });
+
+ 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/20260213192005_AddChatSettings.cs b/src/Purrse.Data/Migrations/20260213192005_AddChatSettings.cs
new file mode 100644
index 0000000..48a3b3f
--- /dev/null
+++ b/src/Purrse.Data/Migrations/20260213192005_AddChatSettings.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Purrse.Data.Migrations
+{
+ ///
+ public partial class AddChatSettings : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "ChatBotName",
+ table: "ai_categorization_settings",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "ChatContextSize",
+ table: "ai_categorization_settings",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "ChatBotName",
+ table: "ai_categorization_settings");
+
+ migrationBuilder.DropColumn(
+ name: "ChatContextSize",
+ table: "ai_categorization_settings");
+ }
+ }
+}
diff --git a/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs b/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs
index 1b19c2e..ebfed8a 100644
--- a/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs
+++ b/src/Purrse.Data/Migrations/PurrseDbContextModelSnapshot.cs
@@ -91,6 +91,13 @@ namespace Purrse.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
+ b.Property("ChatBotName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ChatContextSize")
+ .HasColumnType("integer");
+
b.Property("ConfidenceThreshold")
.HasColumnType("double precision");