import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; const categoryKind = v.union(v.literal("einnahme"), v.literal("ausgabe")); const expenseBlock = v.union(v.literal("wiederkehrend"), v.literal("variabel")); const loanStatus = v.union( v.literal("aktiv"), v.literal("abbezahlt"), v.literal("pausiert"), ); const chatRole = v.union(v.literal("user"), v.literal("assistant")); const chatToolTrace = v.object({ name: v.string(), inputSummary: v.string(), resultSummary: v.string(), }); export default defineSchema({ ...authTables, accounts: defineTable({ userId: v.id("users"), name: v.string(), type: v.string(), iban: v.optional(v.string()), openingBalance: v.number(), currency: v.string(), isArchived: v.boolean(), externalId: v.optional(v.string()), }) .index("by_user", ["userId"]) .index("by_user_external", ["userId", "externalId"]), categories: defineTable({ userId: v.id("users"), name: v.string(), kind: categoryKind, block: v.optional(expenseBlock), color: v.string(), icon: v.optional(v.string()), sortOrder: v.number(), isSystem: v.boolean(), }) .index("by_user", ["userId"]) .index("by_user_kind", ["userId", "kind"]) .index("by_user_name", ["userId", "name"]), transactions: defineTable({ userId: v.id("users"), accountId: v.optional(v.id("accounts")), categoryId: v.optional(v.id("categories")), bookingDate: v.optional(v.string()), valueDate: v.optional(v.string()), description: v.string(), counterparty: v.optional(v.string()), amount: v.number(), vorgang: v.optional(v.string()), isPending: v.boolean(), notes: v.optional(v.string()), rawText: v.optional(v.string()), importId: v.optional(v.id("imports")), assignedMonth: v.optional(v.string()), effectiveMonth: v.optional(v.string()), dedupHash: v.optional(v.string()), externalRef: v.optional(v.string()), }) .index("by_user", ["userId"]) .index("by_user_booking", ["userId", "bookingDate"]) .index("by_user_effmonth", ["userId", "effectiveMonth"]) .index("by_user_category", ["userId", "categoryId"]) .index("by_user_account", ["userId", "accountId"]) .index("by_user_account_booking", ["userId", "accountId", "bookingDate"]) .index("by_user_account_effmonth", ["userId", "accountId", "effectiveMonth"]) .index("by_user_dedup", ["userId", "dedupHash"]) .index("by_user_extref", ["userId", "externalRef"]) .searchIndex("search_description", { searchField: "description", filterFields: ["userId"], }), loans: defineTable({ userId: v.id("users"), name: v.string(), lender: v.optional(v.string()), accountId: v.optional(v.id("accounts")), categoryId: v.optional(v.id("categories")), principal: v.number(), annualInterestRate: v.number(), effectiveAnnualRate: v.optional(v.number()), monthlyPayment: v.optional(v.number()), termMonths: v.optional(v.number()), startDate: v.string(), currentBalance: v.optional(v.number()), totalInterest: v.optional(v.number()), totalAmount: v.optional(v.number()), status: loanStatus, notes: v.optional(v.string()), }) .index("by_user", ["userId"]) .index("by_user_status", ["userId", "status"]), imports: defineTable({ userId: v.id("users"), filename: v.string(), source: v.string(), accountId: v.optional(v.id("accounts")), rowCount: v.number(), importedCount: v.number(), status: v.string(), }).index("by_user", ["userId"]), appSettings: defineTable({ userId: v.id("users"), ownNames: v.array(v.string()), monthBasis: v.union(v.literal("effective"), v.literal("booking")), salaryShift: v.object({ enabled: v.boolean(), categoryNames: v.array(v.string()), dayThreshold: v.number(), }), }).index("by_user", ["userId"]), comdirectSessions: defineTable({ userId: v.id("users"), sessionUuid: v.string(), identifier: v.optional(v.string()), accessToken: v.optional(v.string()), refreshToken: v.optional(v.string()), secondaryActive: v.boolean(), challengeId: v.optional(v.string()), challengeType: v.optional(v.string()), status: v.string(), expiresAt: v.optional(v.number()), }).index("by_user", ["userId"]), bankConfig: defineTable({ userId: v.id("users"), providerPreference: v.union( v.literal("auto"), v.literal("comdirect"), v.literal("fints"), ), comdirectHasCredentials: v.boolean(), fints: v.object({ blz: v.string(), url: v.string(), login: v.string(), productId: v.string(), productVersion: v.optional(v.string()), tanMethodId: v.optional(v.number()), tanMediaName: v.optional(v.string()), bankingInformationJson: v.optional(v.string()), }), }).index("by_user", ["userId"]), syncState: defineTable({ userId: v.id("users"), lastSync: v.optional(v.number()), lastProviderUsed: v.optional(v.union(v.literal("comdirect"), v.literal("fints"))), lastError: v.optional(v.string()), }).index("by_user", ["userId"]), pendingTan: defineTable({ userId: v.id("users"), status: v.union( v.literal("idle"), v.literal("awaiting"), v.literal("done"), v.literal("error"), ), challengeRef: v.optional(v.string()), challengeMessage: v.optional(v.string()), photoTanMimeType: v.optional(v.string()), photoTanBase64: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), errorMessage: v.optional(v.string()), pollAttempt: v.optional(v.number()), syncJobJson: v.optional(v.string()), isDecoupled: v.optional(v.boolean()), submittedTan: v.optional(v.string()), }).index("by_user", ["userId"]), chatSessions: defineTable({ userId: v.id("users"), title: v.string(), createdAt: v.number(), updatedAt: v.number(), messageCount: v.number(), legacyLocalId: v.optional(v.string()), isDeleted: v.boolean(), }) .index("by_user_deleted_updated", ["userId", "isDeleted", "updatedAt"]) .index("by_user_legacyLocalId", ["userId", "legacyLocalId"]), chatMessages: defineTable({ userId: v.id("users"), sessionId: v.id("chatSessions"), role: chatRole, content: v.string(), createdAt: v.number(), toolTrace: v.optional(v.array(chatToolTrace)), }).index("by_user_session_created", ["userId", "sessionId", "createdAt"]), });