Files
finance-app/prisma/schema.prisma
T
siddharthd 689fadc8b9 feat(shared): give a payment a tab to settle
A payment has only ever recorded from, to, amount and date. That is why
the per-trip owed figure did not exist — getTripAnalytics said so where
the number should have been: "split_payments carries no trip attribution,
so a payment cannot be assigned to a trip. Settlement is a property of the
whole relationship." Every trip therefore read 100% unsettled, including
trips paid in full.

It is also why the Shared page silently drops payments under a tag filter.
With one global pool there was nothing honest to subtract, so it showed
gross splits under the same label. A tag is a view; a scope is a ledger.

The scope is a trip, not a new settlement_contexts table. trips already
has owner_id, dates and archived, and transaction_overrides.trip_id
already decides membership. A second grouping beside it would be two
unsynchronised scopes over the same rows, with no invariant saying which
governs. NULL means the ongoing household tab, which never closes.

settled answers a different question and the two must not be collapsed:
trip_id is which tab, settled is whether the obligation is still live.
Critically, a live obligation is NOT settled by flipping the flag — it is
settled by recording the payment, and the balance nets to zero on its own.
Doing both would subtract the settlement twice. So settled is written only
by the historical import, for repayments made on a platform we no longer
run, and there is deliberately no "mark settled" action.

Both owed figures now exclude settled splits and the trip figure nets its
own payments. Spend analytics (myShare/mySplitOf) deliberately still count
settled rows: my half of a 2025 grocery shop is my spend whether or not the
other half was ever repaid, and filtering them would re-inflate exactly the
figures importing settled history exists to correct.

Also drops /api/participants/[id]/balance. It had no consumers, no owner
scoping, no debit/credit signs and no EXCLUDE_RECONCILED_SOURCE — a fourth
balance implementation that disagreed with the others and would have
imported three bugs if anything had aligned to it.

getTripAnalytics had no test at all. It has five now, including the one
that matters: a household payment must not make a trip look paid. Verified
by mutation — neutering the settled filter fails three, and dropping the
trip filter on payments fails that one.
2026-07-27 23:12:05 +10:00

241 lines
9.0 KiB
Plaintext

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model trips {
id Int @id @default(autoincrement())
owner_id Int
name String
description String?
start_date DateTime? @db.Date
end_date DateTime? @db.Date
color String @default("#6366f1")
archived Boolean @default(false)
created_at DateTime @default(now())
overrides transaction_overrides[]
payments split_payments[]
}
model transaction_overrides {
id Int @id @default(autoincrement())
transaction_id Int @unique
merchant_normalized String?
category_override String?
notes String?
my_share_percent Decimal? @db.Decimal(5, 2)
updated_at DateTime @default(now()) @updatedAt
trip_id Int?
trip trips? @relation(fields: [trip_id], references: [id], onDelete: SetNull)
}
model participants {
id Int @id @default(autoincrement())
name String @unique
email String? @unique
created_at DateTime @default(now())
splits transaction_splits[]
account_owner_mappings account_owner_mappings[]
payments_sent split_payments[] @relation("payments_from")
payments_received split_payments[] @relation("payments_to")
}
model account_owner_mappings {
id Int @id @default(autoincrement())
bank_name String
account_number String
owner_id Int
created_at DateTime @default(now())
owner participants @relation(fields: [owner_id], references: [id])
@@unique([bank_name, account_number])
}
model transaction_splits {
id Int @id @default(autoincrement())
transaction_id Int
participant_id Int
share_percent Decimal @db.Decimal(5, 2)
settled Boolean @default(false)
settled_at DateTime?
created_at DateTime @default(now())
participant participants @relation(fields: [participant_id], references: [id])
@@unique([transaction_id, participant_id])
}
model split_payments {
id Int @id @default(autoincrement())
from_participant_id Int
to_participant_id Int
amount Decimal @db.Decimal(10, 2)
payment_date DateTime @db.Date
notes String?
linked_transaction_id Int?
trip_id Int?
created_at DateTime @default(now())
from_participant participants @relation("payments_from", fields: [from_participant_id], references: [id])
to_participant participants @relation("payments_to", fields: [to_participant_id], references: [id])
trip trips? @relation(fields: [trip_id], references: [id], onDelete: SetNull)
@@index([trip_id])
}
model tags {
id Int @id @default(autoincrement())
name String @unique
color String @default("#6366f1")
created_at DateTime @default(now())
transaction_tags transaction_tags[]
}
model transaction_tags {
transaction_id Int
tag_id Int
created_at DateTime @default(now())
tag tags @relation(fields: [tag_id], references: [id], onDelete: Cascade)
@@id([transaction_id, tag_id])
}
model rules {
id Int @id @default(autoincrement())
owner_id Int
name String
conditions Json @default("[]")
actions Json @default("{}")
enabled Boolean @default(true)
manual_only Boolean @default(false)
priority Int @default(0)
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
}
model budgets {
id Int @id @default(autoincrement())
owner_id Int
category String
month DateTime @db.Date
amount_limit Decimal @db.Decimal(10, 2)
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
@@unique([owner_id, category, month])
}
model statements {
id Int @id @default(autoincrement())
filename String
bank_name String?
card_name String?
account_type String?
account_number String
billing_start_date DateTime? @db.Date
billing_end_date DateTime? @db.Date
total_amount_due Decimal? @db.Decimal(12, 2)
minimum_amount_due Decimal? @db.Decimal(12, 2)
payment_due_date DateTime? @db.Date
event_created Boolean? @default(false)
tier_used String?
created_at DateTime? @default(now())
statement_type String @default("other")
currency String? @default("AUD")
opening_balance Decimal? @db.Decimal(12, 2)
closing_balance Decimal? @db.Decimal(12, 2)
total_credits Decimal? @db.Decimal(12, 2)
total_debits Decimal? @db.Decimal(12, 2)
interest_charged Decimal? @db.Decimal(12, 2)
fees_charged Decimal? @db.Decimal(12, 2)
credit_limit Decimal? @db.Decimal(12, 2)
available_credit Decimal? @db.Decimal(12, 2)
owner_id Int @default(1)
account_holder_name String?
exchange_rate_to_aud Decimal? @db.Decimal(10, 6)
paperless_doc_id Int? @unique
interest_rate Decimal? @db.Decimal(6, 3)
scheduled_repayment Decimal? @db.Decimal(12, 2)
repayment_frequency String?
redraw_available Decimal? @db.Decimal(12, 2)
loan_term_months Int?
transactions transactions[]
}
model transactions {
id Int @id @default(autoincrement())
statement_id Int?
transaction_date DateTime @db.Date
description String?
amount Decimal @db.Decimal(12, 2)
created_at DateTime? @default(now())
transaction_type String? @default("debit")
merchant_name String?
location String?
foreign_currency_amount Decimal? @db.Decimal(12, 2)
foreign_currency_code String?
category String?
row_index Int?
merchant_normalized String?
amount_aud Decimal? @db.Decimal(12, 2)
payment_method String? // card | cash | bank_transfer | other; NULL = unknown (migration 0016)
owner_id Int?
reconciled_with_id Int?
principal_amount Decimal? @db.Decimal(12, 2)
interest_amount Decimal? @db.Decimal(12, 2)
statement statements? @relation(fields: [statement_id], references: [id], onDelete: Cascade)
reconciled_with transactions? @relation("reconciled", fields: [reconciled_with_id], references: [id], onDelete: SetNull)
reconciled_by transactions[] @relation("reconciled")
expense_metadata expense_metadata?
order_review order_reviews?
}
model expense_metadata {
id Int @id @default(autoincrement())
transaction_id Int? @unique
source String @default("email")
paperless_doc_id Int? @unique
source_email_subject String?
source_email_from String?
source_message_id String?
payment_method String?
payment_method_detail String?
order_reference String?
line_items Json @default("[]")
tax_amount Decimal? @db.Decimal(12, 2)
subtotal Decimal? @db.Decimal(12, 2)
merchant_normalized String?
amount Decimal? @db.Decimal(12, 2)
transaction_date DateTime? @db.Date
extraction_model String? @default("gemini-2.5-flash")
created_at DateTime? @default(now())
transaction transactions? @relation(fields: [transaction_id], references: [id], onDelete: Cascade)
@@unique([source, order_reference], name: "uq_expense_source_order")
}
model order_reviews {
id Int @id @default(autoincrement())
transaction_id Int @unique
rating String?
order_again Boolean?
note String?
item_verdicts Json @default("[]")
created_at DateTime @default(now())
updated_at DateTime @updatedAt
transaction transactions @relation(fields: [transaction_id], references: [id], onDelete: Cascade)
}
model rule_apply_runs {
id Int @id @default(autoincrement())
owner_id Int
applied_at DateTime @default(now())
split_from DateTime? @db.Date
matched Int @default(0)
transactions_affected Int @default(0)
reverted_at DateTime?
snapshot Json @default("[]")
}