add money tracker
All checks were successful
Test and deploy staging / staging (push) Successful in 1m46s

This commit is contained in:
2026-09-23 17:02:32 +00:00
parent 753b2e5d6a
commit 0638ead87f
15 changed files with 2532 additions and 3 deletions

View File

@@ -30,8 +30,15 @@ try {
to_regclass('public.list_item_completions') is not null as list_completions,
(select count(*) from information_schema.columns where table_name = 'tasks_to_topics' and column_name = 'id') as topic_link_id,
(select count(*) from information_schema.columns
where table_name in ('animals', 'sightings', 'photos')
and column_name = 'id' and data_type = 'integer') as animal_serial_ids
where table_name in ('animals', 'sightings', 'photos')
and column_name = 'id' and data_type = 'integer') as animal_serial_ids,
(select count(*) from information_schema.tables
where table_schema = 'public' and table_name in (
'money_accounts', 'money_transactions', 'money_recurring', 'money_forecasts',
'money_utility_settings', 'money_utility_readings', 'money_wishlist', 'money_insurances'
)) as money_tables,
(select count(*) from information_schema.columns
where table_name = 'money_utility_settings' and column_name = 'billing_start') as money_billing_start
`;
if (
!Number(schema.user_role) ||
@@ -41,7 +48,9 @@ try {
!Number(schema.list_slug) ||
!schema.list_completions ||
!Number(schema.topic_link_id) ||
Number(schema.animal_serial_ids) !== 3
Number(schema.animal_serial_ids) !== 3 ||
Number(schema.money_tables) !== 8 ||
!Number(schema.money_billing_start)
) {
throw new Error(
'Required migrations or Animaldex ID conversion have not been applied to this database'

View File

@@ -0,0 +1,25 @@
-- One-off bridge from the pre-foundation site schema to the current table names.
-- Run after the Animaldex UUID conversion, if needed, and before the auth,
-- foundation, and catalog migrations. Review and back up each database first.
-- Renaming keeps existing rows and foreign keys; lists did not exist in this schema.
BEGIN;
ALTER TABLE public.game RENAME TO games;
ALTER TABLE public.pokemon_game_entry RENAME TO pokemon_game_entries;
ALTER TABLE public.user_entry RENAME TO user_entries;
ALTER TABLE public.user_game RENAME TO user_games;
CREATE TABLE public.lists (
id serial PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE public.list_items (
id serial PRIMARY KEY,
list_id integer NOT NULL REFERENCES public.lists(id) ON DELETE CASCADE,
name text NOT NULL
);
CREATE INDEX list_items_list_id_idx ON public.list_items(list_id);
COMMIT;

View File

@@ -0,0 +1,15 @@
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
if (!process.env.DB_URL) throw new Error('DB_URL is required');
const client = postgres(process.env.DB_URL, { max: 1 });
try {
await client.begin(async (tx) => {
await tx.unsafe(
await readFile(new URL('./money-tracker-migration.sql', import.meta.url), 'utf8')
);
});
console.log('Money tracker migration applied');
} finally {
await client.end();
}

View File

@@ -0,0 +1,95 @@
CREATE TABLE IF NOT EXISTS money_accounts (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
name text NOT NULL,
opening_balance_cents integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (user_id, name)
);
CREATE INDEX IF NOT EXISTS money_accounts_user_id_idx ON money_accounts (user_id);
CREATE TABLE IF NOT EXISTS money_transactions (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
account_id integer NOT NULL REFERENCES money_accounts(id),
date date NOT NULL,
description text NOT NULL,
amount_cents integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_transactions_user_date_idx ON money_transactions (user_id, date, id);
CREATE INDEX IF NOT EXISTS money_transactions_account_id_idx ON money_transactions (account_id);
CREATE TABLE IF NOT EXISTS money_recurring (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
name text NOT NULL,
amount_cents integer NOT NULL,
cadence text NOT NULL,
account_id integer REFERENCES money_accounts(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_recurring_user_id_idx ON money_recurring (user_id);
CREATE TABLE IF NOT EXISTS money_forecasts (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
date date NOT NULL,
description text NOT NULL,
amount_cents integer NOT NULL,
account_id integer REFERENCES money_accounts(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_forecasts_user_date_idx ON money_forecasts (user_id, date);
CREATE TABLE IF NOT EXISTS money_utility_settings (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
kind text NOT NULL,
rate_per_unit numeric(12, 4) NOT NULL DEFAULT 0,
monthly_advance_cents integer NOT NULL DEFAULT 0,
billing_start date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (user_id, kind)
);
ALTER TABLE money_utility_settings ADD COLUMN IF NOT EXISTS billing_start date;
CREATE TABLE IF NOT EXISTS money_utility_readings (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
kind text NOT NULL,
date date NOT NULL,
reading numeric(12, 3) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_utility_readings_user_kind_date_idx ON money_utility_readings (user_id, kind, date);
CREATE TABLE IF NOT EXISTS money_wishlist (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
name text NOT NULL,
estimated_cents integer,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_wishlist_user_id_idx ON money_wishlist (user_id);
CREATE TABLE IF NOT EXISTS money_insurances (
id serial PRIMARY KEY,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
name text NOT NULL,
provider text,
coverage text,
premium_cents integer,
cadence text NOT NULL DEFAULT 'monthly',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS money_insurances_user_id_idx ON money_insurances (user_id);