ci / lint-test (push) Successful in 48s
Adds /api/receipts/ingest as a sibling to the order lane, sharing its shape but making one decision differently: nothing is parked. An order can wait for its statement because it is already visible as an email; a gift-card grocery shop is visible nowhere at all, so a scan that produces no transaction produces nothing anyone can see. Every payment becomes a manual row immediately and the existing pending-reconciliation queue resolves the ones with a card leg coming. One transaction per tender leg. A $114.57 shop settled $40.75 gift card + $73.82 Mastercard has a statement line for $73.82 only. A single row marked credits is excluded from the queue while that line double-counts; marked card it is searched for at 1% of $114.57 and never matches. Either way the shop books $188.39. Per-leg rows make each amount the settled amount, so the matcher works untouched. Reconciliation now carries expense_metadata across. It already moved overrides, tags and splits from the manual row to the statement row and left metadata behind, which did not matter while metadata only came from an email that made its own transaction. It matters now that it carries a shop's line items: unmoved, the contents vanish at exactly the moment the statement line appears, and COLES 0556 MANOR LAKES stays as unreadable as before anything was scanned. transaction_id is UNIQUE, so a statement row that already has metadata keeps it and the pantry row is flagged rather than raising a constraint violation. Also regenerates the Prisma model. card_last4, currency, flags, reconciled_at, matched_transaction_id, platform and route have been in the database since migrations 0019/0020 and were absent from schema.prisma — regenerating the client from it would have dropped columns the order lane writes on every ingest. 23 integration tests against the real schema, built from the three receipts that drove the design. Existing suites unchanged: 104 unit, 144 integration.
263 lines
10 KiB
Plaintext
263 lines
10 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())
|
|
// In the database since migrations 0019/0020 but absent from this model until 0027.
|
|
// Regenerating the client from the stale definition would have dropped columns the order
|
|
// lane writes on every ingest.
|
|
card_last4 String?
|
|
currency String?
|
|
flags Json @default("[]")
|
|
reconciled_at DateTime? @db.Timestamptz(6)
|
|
matched_transaction_id Int?
|
|
platform String?
|
|
route Json?
|
|
// 0027 — receipt scans as a second producer on this lane.
|
|
tender_raw String?
|
|
receipt_sha256 String?
|
|
receipt_group String?
|
|
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("[]")
|
|
}
|