money: bank-sync schema (account scope/iban/external uid, transaction external id/source, bank_sessions)

This commit is contained in:
2026-10-24 00:50:45 +00:00
parent 064d41595f
commit b1ce715ec7
3 changed files with 11676 additions and 4 deletions

View File

@@ -1,7 +1,17 @@
import { boolean, check, date, index, integer, numeric, text, unique } from 'drizzle-orm/pg-core';
import {
boolean,
check,
date,
index,
integer,
jsonb,
numeric,
text,
unique
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { moneySchema } from './schemas';
import { id, timestamps } from './helpers';
import { id, timestamps, timestamptz } from './helpers';
import { user } from './auth';
export const moneyAccounts = moneySchema.table(
@@ -15,12 +25,22 @@ export const moneyAccounts = moneySchema.table(
openingBalanceCents: integer('opening_balance_cents').notNull().default(0),
kind: text('kind').notNull().default('spendable'),
color: text('color'),
// Household scope: Stan's own money vs. the joint account with Juhee.
scope: text('scope').notNull().default('personal'),
owner: text('owner'),
iban: text('iban'),
// Bank-side account identifier (Enable Banking account uid) for synced accounts.
externalUid: text('external_uid'),
provider: text('provider').notNull().default('manual'),
...timestamps
},
(table) => [
unique().on(table.userId, table.name),
unique('accounts_external_uid_key').on(table.externalUid),
index().on(table.userId),
check('accounts_kind_check', sql`kind IN ('spendable','investment')`)
check('accounts_kind_check', sql`kind IN ('spendable','investment')`),
check('accounts_scope_check', sql`scope IN ('personal','joint')`),
check('accounts_provider_check', sql`provider IN ('manual','enablebanking')`)
]
);
@@ -37,9 +57,53 @@ export const moneyTransactions = moneySchema.table(
date: date('date', { mode: 'string' }).notNull(),
description: text('description').notNull(),
amountCents: integer('amount_cents').notNull(),
// Bank transaction id (Enable Banking entry_reference / transaction_id, or a
// content hash for CSV imports). NULLs are distinct, so manual rows never clash.
externalId: text('external_id'),
source: text('source').notNull().default('manual'),
bookingStatus: text('booking_status'),
counterparty: text('counterparty'),
category: text('category'),
raw: jsonb('raw'),
valueDate: date('value_date', { mode: 'string' }),
...timestamps
},
(table) => [index().on(table.userId, table.date, table.id), index().on(table.accountId)]
(table) => [
index().on(table.userId, table.date, table.id),
index().on(table.accountId),
unique('transactions_account_external_id_key').on(table.accountId, table.externalId),
check('transactions_source_check', sql`source IN ('manual','csv','enablebanking')`)
]
);
// Open-banking consent sessions (Enable Banking). One row per authorised session;
// `accounts` keeps the bank-side account list returned when the session was created.
export const moneyBankSessions = moneySchema.table(
'bank_sessions',
{
id: id(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
provider: text('provider').notNull().default('enablebanking'),
sessionId: text('session_id').notNull(),
aspspName: text('aspsp_name').notNull(),
aspspCountry: text('aspsp_country').notNull(),
psuType: text('psu_type'),
status: text('status').notNull().default('authorized'),
validUntil: timestamptz('valid_until'),
accounts: jsonb('accounts')
.notNull()
.default(sql`'[]'::jsonb`),
lastSyncedAt: timestamptz('last_synced_at'),
lastError: text('last_error'),
...timestamps
},
(table) => [
unique('bank_sessions_session_id_key').on(table.sessionId),
index().on(table.userId),
check('bank_sessions_status_check', sql`status IN ('authorized','expired','revoked','error')`)
]
);
export const moneyRecurring = moneySchema.table(