Files
TehRiehlBudget/tehriehlbudget-backend/prisma/schema.prisma
T
TehRiehlDeal a8a47e38c1
CI / secrets-scan (push) Successful in 8s
CI / sast (push) Successful in 13s
CI / vuln-scan (push) Successful in 15s
CI / test (push) Failing after 29s
CI / lint (push) Failing after 31s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Add bulk statement-import feature for transactions
Lets a user upload a CSV, OFX/QFX, or PDF bank statement, parses the
transactions, flags duplicates and possible transfers against existing
records, and bulk-creates the accepted rows after a final read-only
confirmation step.

Backend
- New `statements` module with CSV (papaparse), OFX/QFX (node-ofx-parser),
  and PDF (pdf-parse) parsers behind a `StatementParser` strategy
  interface; format detected by content sniffing + extension.
- `DuplicateDetectorService` checks FITID/externalId exact matches first,
  then a date+amount+description Jaro-Winkler heuristic, then cross-account
  transfer pairing.
- New `POST /statements/parse` (multipart, in-memory, 10MB cap) returns the
  parsed preview without writing, including per-row `status` (`new`,
  `duplicate`, `needs_review`, `possible_transfer`) and any `needsMapping`
  payload when CSV headers are unrecognized.
- `POST /transactions/bulk` accepts up to 500 rows, chunks them 50 at a
  time inside `prisma.$transaction`, applies balance deltas, and writes a
  single `ActivityLog` row per chunk instead of one per transaction.
- Schema: nullable `external_id` column on `Transaction` plus composite
  indexes on `(account_id, external_id)` and `(account_id, date)` for fast
  dedupe-window queries. Not encrypted — it's an opaque bank ID used as a
  lookup key.

Frontend
- `ImportStatementDialog` runs a 4-step wizard: Upload → Column Mapping (if
  needed) → Review (editable table with duplicate/transfer badges) →
  Confirm (read-only summary with projected per-account balance impact and
  count-bearing primary button). The Confirm step gates the actual write,
  and Back to Review preserves all edit/checkbox state.
- New `bulkCreateTransactions` action on the transactions store.
- "Import Statement" button added next to Export on the Transactions page,
  with a success toast and a refresh of the transactions + accounts stores.

Tests
- 306 backend tests (29 suites), 195 frontend tests (31 suites), all green.
- Fixtures under `test/fixtures/statements/` cover three CSV sign
  conventions (signed-amount, debit/credit, credit-card), OFX 1.x SGML,
  OFX 2.x XML, and a credit-card QFX with the CCSTMTRS branch.

Versions bumped to 0.4.0 on both packages per the lockstep rule.

NOTE: the Prisma migration in
`prisma/migrations/20260527203542_add_transaction_external_id/` still
needs to be applied to the live database with `prisma migrate deploy` —
the DB at 10.0.3.82 wasn't reachable from the dev environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:23:45 -07:00

149 lines
4.5 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
supabaseId String @unique @map("supabase_id")
email String @unique
name String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
accounts Account[]
transactions Transaction[]
categories Category[]
activityLogs ActivityLog[]
@@map("users")
}
model Account {
id String @id @default(uuid())
userId String @map("user_id")
name String
type AccountType
balance Decimal @default(0) @db.Decimal(12, 2)
institution String?
accountNumber String? @map("account_number")
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transactions Transaction[] @relation("AccountTransactions")
incomingTransfers Transaction[] @relation("DestinationAccountTransactions")
valuations AccountValuation[]
@@map("accounts")
}
enum AccountType {
CHECKING
SAVINGS
CREDIT
LOAN
STOCK
CASH
INVESTMENT
RETIREMENT
}
model AccountValuation {
id String @id @default(uuid())
accountId String @map("account_id")
date DateTime
value Decimal @db.Decimal(12, 2)
createdAt DateTime @default(now()) @map("created_at")
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@index([accountId, date])
@@map("account_valuations")
}
model Transaction {
id String @id @default(uuid())
userId String @map("user_id")
accountId String @map("account_id")
destinationAccountId String? @map("destination_account_id")
categoryId String? @map("category_id")
amount Decimal @db.Decimal(12, 2)
type TransactionType
description String
notes String?
date DateTime
receiptPath String? @map("receipt_path")
externalId String? @map("external_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
account Account @relation("AccountTransactions", fields: [accountId], references: [id], onDelete: Cascade)
destinationAccount Account? @relation("DestinationAccountTransactions", fields: [destinationAccountId], references: [id], onDelete: Cascade)
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
@@index([accountId, externalId])
@@index([accountId, date])
@@map("transactions")
}
enum TransactionType {
INCOME
EXPENSE
TRANSFER
}
model Category {
id String @id @default(uuid())
userId String @map("user_id")
name String
color String?
icon String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transactions Transaction[]
@@unique([userId, name])
@@map("categories")
}
enum EntityType {
TRANSACTION
ACCOUNT
ACCOUNT_VALUATION
}
enum ActivityAction {
CREATE
UPDATE
DELETE
}
model ActivityLog {
id String @id @default(uuid())
userId String @map("user_id")
entityType EntityType @map("entity_type")
entityId String @map("entity_id")
action ActivityAction
accountId String? @map("account_id")
destinationAccountId String? @map("destination_account_id")
summary String?
snapshot Json?
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, createdAt])
@@index([userId, accountId, createdAt])
@@index([userId, destinationAccountId, createdAt])
@@index([userId, entityType, createdAt])
@@map("activity_logs")
}