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'), // Last balance reported by the bank (synced accounts); the source of truth for the total. bankBalanceCents: integer('bank_balance_cents'), bankBalanceType: text('bank_balance_type'), bankBalanceAt: timestamptz('bank_balance_at'), ...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')`) ] );