generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique passwordHash String fullName String? phone String? companyName String? addressLine1 String? addressLine2 String? city String? state String? postalCode String? country String? role String @default("user") emailVerified Boolean @default(false) twoFactorEnabled Boolean @default(false) twoFactorSecret String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accounts Account[] rules Rule[] exports ExportLog[] exportDownloadTokens ExportDownloadToken[] auditLogs AuditLog[] abuseEvents AbuseEvent[] googleConnection GoogleConnection? apiKeys ApiKey[] emailVerificationToken EmailVerificationToken? passwordResetTokens PasswordResetToken[] refreshTokens RefreshToken[] sessions Session[] socialAccounts SocialAccount[] subscription Subscription? taxReturns TaxReturn[] csvImportMappings CsvImportMapping[] createdHouseholds Household[] @relation("HouseholdCreator") householdMemberships HouseholdMember[] sentHouseholdInvites HouseholdInvite[] @relation("HouseholdInviteInviter") acceptedHouseholdInvites HouseholdInvite[] @relation("HouseholdInviteAccepter") ownedAccounts Account[] @relation("AccountOwnerUser") createdHouseholdGoals HouseholdGoal[] @relation("HouseholdGoalCreator") } model Household { id String @id @default(uuid()) name String createdByUserId String metadata Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt createdBy User @relation("HouseholdCreator", fields: [createdByUserId], references: [id], onDelete: Cascade) members HouseholdMember[] invites HouseholdInvite[] accounts Account[] goals HouseholdGoal[] @@index([createdByUserId, createdAt]) } model HouseholdGoal { id String @id @default(uuid()) householdId String createdByUserId String name String description String? targetAmount Decimal currentAmount Decimal @default(0) isoCurrencyCode String @default("USD") targetDate DateTime? priority String @default("medium") status String @default("active") metadata Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt household Household @relation(fields: [householdId], references: [id], onDelete: Cascade) createdBy User @relation("HouseholdGoalCreator", fields: [createdByUserId], references: [id], onDelete: Cascade) @@index([householdId, status]) @@index([createdByUserId, createdAt]) @@index([targetDate]) } model HouseholdMember { id String @id @default(uuid()) householdId String userId String role String @default("member") status String @default("active") joinedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt household Household @relation(fields: [householdId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([householdId, userId]) @@index([userId, status]) @@index([householdId, role]) } model HouseholdInvite { id String @id @default(uuid()) householdId String invitedById String email String role String @default("member") tokenHash String @unique status String @default("pending") expiresAt DateTime acceptedAt DateTime? acceptedById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt household Household @relation(fields: [householdId], references: [id], onDelete: Cascade) invitedBy User @relation("HouseholdInviteInviter", fields: [invitedById], references: [id], onDelete: Cascade) acceptedBy User? @relation("HouseholdInviteAccepter", fields: [acceptedById], references: [id], onDelete: SetNull) @@index([householdId, status]) @@index([email, status]) @@index([expiresAt]) } model SocialAccount { id String @id @default(uuid()) userId String provider String providerAccountId String email String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@index([userId]) } model Account { id String @id @default(uuid()) userId String householdId String? ownerUserId String? ownershipType String @default("mine") institutionName String accountType String mask String? plaidAccessToken String? plaidItemId String? plaidAccountId String? @unique tellerAccessToken String? tellerEnrollmentId String? tellerAccountId String? @unique currentBalance Decimal? availableBalance Decimal? isoCurrencyCode String? lastBalanceSync DateTime? lastTransactionSync DateTime? lastSyncAttemptAt DateTime? syncStatus String @default("idle") lastSyncError String? syncConsecutiveFailures Int @default(0) plaidWebhookCode String? plaidWebhookAt DateTime? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) household Household? @relation(fields: [householdId], references: [id], onDelete: SetNull) ownerUser User? @relation("AccountOwnerUser", fields: [ownerUserId], references: [id], onDelete: SetNull) transactionsRaw TransactionRaw[] @@index([householdId, ownershipType]) @@index([ownerUserId]) @@index([plaidItemId]) @@index([tellerEnrollmentId]) } model PlaidWebhookEvent { id String @id @default(uuid()) itemId String? webhookType String webhookCode String payload Json status String @default("received") error String? receivedAt DateTime @default(now()) processedAt DateTime? @@index([itemId, receivedAt]) @@index([webhookType, webhookCode]) } model TransactionRaw { id String @id @default(uuid()) accountId String bankTransactionId String @unique date DateTime amount Decimal description String rawPayload Json ingestedAt DateTime @default(now()) source String account Account @relation(fields: [accountId], references: [id]) derived TransactionDerived? ruleExecutions RuleExecution[] } model TransactionDerived { id String @id @default(uuid()) rawTransactionId String @unique userCategory String? userNotes String? attribution String @default("mine") splitMode String @default("none") splitMinePercent Decimal? splitYoursPercent Decimal? isHidden Boolean @default(false) modifiedAt DateTime @default(now()) modifiedBy String raw TransactionRaw @relation(fields: [rawTransactionId], references: [id]) } model CsvImportMapping { id String @id @default(uuid()) userId String headerSignature String name String? mapping Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastUsedAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, headerSignature]) @@index([userId, lastUsedAt]) } model Rule { id String @id @default(uuid()) userId String name String priority Int conditions Json actions Json isActive Boolean @default(true) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) executions RuleExecution[] } model RuleExecution { id String @id @default(uuid()) ruleId String transactionId String executedAt DateTime @default(now()) result Json rule Rule @relation(fields: [ruleId], references: [id]) transaction TransactionRaw @relation(fields: [transactionId], references: [id]) } model ExportLog { id String @id @default(uuid()) userId String format String @default("csv") destination String @default("download") filters Json rowCount Int fileName String? mimeType String? fileHash String? ipAddress String? userAgent String? metadata Json @default("{}") createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId, createdAt]) @@index([format, createdAt]) } model ExportDownloadToken { id String @id @default(uuid()) userId String tokenHash String @unique format String filters Json storageProvider String? storageKey String? fileName String? mimeType String? rowCount Int? fileHash String? expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt]) @@index([expiresAt]) } model AuditLog { id String @id @default(uuid()) userId String action String metadata Json createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) } model AbuseEvent { id String @id @default(uuid()) userId String? eventType String riskPoints Int severity String ipAddress String? userAgent String? metadata Json @default("{}") createdAt DateTime @default(now()) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([userId, createdAt]) @@index([eventType, createdAt]) } model GoogleConnection { id String @id @default(uuid()) userId String @unique googleEmail String refreshToken String accessToken String? spreadsheetId String? driveMirrorEnabled Boolean @default(false) driveMirrorStatus String @default("not_started") driveMirrorSpreadsheetUrl String? driveMirrorLastSyncedAt DateTime? isConnected Boolean @default(true) connectedAt DateTime @default(now()) lastSyncedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model ApiKey { id String @id @default(uuid()) userId String name String prefix String keyHash String @unique scopes String[] @default(["transactions:read"]) lastUsedAt DateTime? revokedAt DateTime? expiresAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt]) @@index([prefix]) } model EmailVerificationToken { id String @id @default(uuid()) userId String @unique token String @unique expiresAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model PasswordResetToken { id String @id @default(uuid()) userId String token String @unique expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Session { id String @id @default(uuid()) userId String ipHash String userAgentHash String createdAt DateTime @default(now()) lastSeenAt DateTime @default(now()) expiresAt DateTime revokedAt DateTime? user User @relation(fields: [userId], references: [id], onDelete: Cascade) refreshTokens RefreshToken[] @@index([userId, revokedAt]) } model RefreshToken { id String @id @default(uuid()) userId String sessionId String? tokenHash String @unique expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) session Session? @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@index([sessionId]) } model Subscription { id String @id @default(uuid()) userId String @unique plan String @default("free") stripeCustomerId String? stripeSubId String? currentPeriodEnd DateTime? cancelAtPeriodEnd Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model TaxReturn { id String @id @default(uuid()) userId String taxYear Int filingType String jurisdictions Json status String @default("draft") summary Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) documents TaxDocument[] } model TaxDocument { id String @id @default(uuid()) taxReturnId String docType String metadata Json @default("{}") createdAt DateTime @default(now()) taxReturn TaxReturn @relation(fields: [taxReturnId], references: [id], onDelete: Cascade) }