The ledger already knew we had ordered from a place; it did not know the food was bad. Orders got repeated from places we disliked because nobody remembered by the time the next one went in. That is what the receipt ingestion was for (ING-9) and the last piece was missing: order_reviews existed as a table with no API, no UI and no writes. Four levels, not three. "Loved" and "liked" are both "would order again" but only one is worth a detour, and "ok" is not a recommendation. A verdict belongs to a person, not to an order. A shared meal produces two opinions and they routinely disagree — that disagreement is the useful part, and the old UNIQUE on transaction_id alone could not hold it. Now UNIQUE (transaction_id, participant_id), and the default is the signed-in user rather than the owner: Sonu authenticates through the same Traefik OAuth as participant 4, so an owner default would have filed her verdict under his name. Per-item opinions key on the item DESCRIPTION, not its index. An index is meaningless across orders; "the Pad Thai here is good" is the signal that has to survive into the next order from the same merchant. Only the two poles are offered — a per-item "ok" answers neither of the questions you ask at order time. Sharing is recorded as a real 50/50 split, not a decorative flag. The split already IS the record that an order was shared, and two records of one fact drift apart. An ABSENT item_verdicts means "leave them alone"; an empty array clears them. Without that distinction a note-only save silently wipes every per-item opinion — the same shape as the bug that reset `settled` on split rewrites, and just as invisible on screen. Mutation-tested: making keepItems a no-op fails exactly one test. mockDbWithPool gained queryRow. Omitting an export from the mock makes it undefined at the call site, which fails as "not a function" and reads like a code bug rather than a test-harness gap.
249 lines
9.5 KiB
Plaintext
249 lines
9.5 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")
|
|
order_reviews order_reviews[]
|
|
}
|
|
|
|
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?
|
|
superseded_by_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")
|
|
superseded_by transactions? @relation("superseded", fields: [superseded_by_id], references: [id], onDelete: SetNull)
|
|
supersedes transactions[] @relation("superseded")
|
|
expense_metadata expense_metadata?
|
|
order_reviews 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
|
|
participant_id Int
|
|
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)
|
|
participant participants @relation(fields: [participant_id], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([transaction_id, participant_id], name: "order_reviews_transaction_participant_key")
|
|
}
|
|
|
|
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("[]")
|
|
}
|