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.
241 lines
9.0 KiB
Plaintext
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("[]")
|
|
}
|