247 lines
7.5 KiB
TypeScript
247 lines
7.5 KiB
TypeScript
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, timestamptz } from './helpers';
|
|
import { user } from './auth';
|
|
|
|
export const moneyAccounts = moneySchema.table(
|
|
'accounts',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
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_scope_check', sql`scope IN ('personal','joint')`),
|
|
check('accounts_provider_check', sql`provider IN ('manual','enablebanking')`)
|
|
]
|
|
);
|
|
|
|
export const moneyTransactions = moneySchema.table(
|
|
'transactions',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
accountId: integer('account_id')
|
|
.notNull()
|
|
.references(() => moneyAccounts.id),
|
|
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),
|
|
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(
|
|
'recurring',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
amountCents: integer('amount_cents').notNull(),
|
|
cadence: text('cadence').notNull(),
|
|
startsOn: date('starts_on', { mode: 'string' }),
|
|
endsOn: date('ends_on', { mode: 'string' }),
|
|
accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
index().on(table.userId),
|
|
check('recurring_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
|
|
]
|
|
);
|
|
|
|
export const moneyForecasts = moneySchema.table(
|
|
'forecasts',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
date: date('date', { mode: 'string' }).notNull(),
|
|
description: text('description').notNull(),
|
|
amountCents: integer('amount_cents').notNull(),
|
|
accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }),
|
|
...timestamps
|
|
},
|
|
(table) => [index().on(table.userId, table.date)]
|
|
);
|
|
|
|
export const moneyUtilitySettings = moneySchema.table(
|
|
'utility_settings',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
kind: text('kind').notNull(),
|
|
ratePerUnit: numeric('rate_per_unit', { precision: 12, scale: 4 }).notNull().default('0'),
|
|
monthlyAdvanceCents: integer('monthly_advance_cents').notNull().default(0),
|
|
billingStart: date('billing_start', { mode: 'string' }),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
unique().on(table.userId, table.kind),
|
|
check('utility_settings_kind_check', sql`kind IN ('electricity','gas','water')`)
|
|
]
|
|
);
|
|
|
|
export const moneyUtilityReadings = moneySchema.table(
|
|
'utility_readings',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
kind: text('kind').notNull(),
|
|
date: date('date', { mode: 'string' }).notNull(),
|
|
reading: numeric('reading', { precision: 12, scale: 3 }).notNull(),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
index().on(table.userId, table.kind, table.date),
|
|
check('utility_readings_kind_check', sql`kind IN ('electricity','gas','water')`)
|
|
]
|
|
);
|
|
|
|
export const moneyWishlist = moneySchema.table(
|
|
'wishlist',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
estimatedCents: integer('estimated_cents'),
|
|
priority: integer('priority').notNull().default(3),
|
|
notes: text('notes'),
|
|
...timestamps
|
|
},
|
|
(table) => [index().on(table.userId)]
|
|
);
|
|
|
|
export const moneyProjectionSettings = moneySchema.table(
|
|
'projection_settings',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
investmentAccountId: integer('investment_account_id').references(() => moneyAccounts.id, {
|
|
onDelete: 'set null'
|
|
}),
|
|
reserveCents: integer('reserve_cents').notNull().default(100000),
|
|
annualReturnPercent: numeric('annual_return_percent', { precision: 5, scale: 2 })
|
|
.notNull()
|
|
.default('5.00'),
|
|
contributionMode: text('contribution_mode').notNull().default('excess'),
|
|
includeRecurring: boolean('include_recurring').notNull().default(true),
|
|
fixedMonthlyCents: integer('fixed_monthly_cents').notNull().default(0),
|
|
endDate: date('end_date', { mode: 'string' }),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
unique().on(table.userId),
|
|
check(
|
|
'projection_settings_contribution_mode_check',
|
|
sql`contribution_mode IN ('excess','fixed')`
|
|
)
|
|
]
|
|
);
|
|
|
|
export const moneyInsurances = moneySchema.table(
|
|
'insurances',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
provider: text('provider'),
|
|
coverage: text('coverage'),
|
|
premiumCents: integer('premium_cents'),
|
|
cadence: text('cadence').notNull().default('monthly'),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
index().on(table.userId),
|
|
check('insurances_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
|
|
]
|
|
);
|