first commit

This commit is contained in:
Alessio
2026-06-02 04:02:59 +02:00
parent 368f69fdce
commit 8e5f4aafa2
148 changed files with 10590 additions and 110 deletions

150
database/init.sql Normal file
View File

@@ -0,0 +1,150 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
email text NOT NULL UNIQUE,
password_hash text NOT NULL,
avatar_file text,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE TABLE sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash text NOT NULL UNIQUE,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE INDEX sessions_user_id_idx ON sessions(user_id);
CREATE INDEX sessions_expires_at_idx ON sessions(expires_at);
CREATE TABLE subscriptions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
amount double precision NOT NULL,
billing_cycle text NOT NULL,
activation_date text NOT NULL,
first_payment_date text NOT NULL,
end_date text,
occurrence_count integer CHECK (occurrence_count > 0),
custom_interval_value integer CHECK (custom_interval_value > 0),
custom_interval_unit text CHECK (
custom_interval_unit IN ('days', 'weeks', 'months', 'years')
),
subscription_status text NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL,
CONSTRAINT subscriptions_amount_positive CHECK (amount > 0),
CONSTRAINT subscriptions_billing_cycle_valid CHECK (
billing_cycle IN ('weekly', 'monthly', 'yearly', 'custom')
),
CONSTRAINT subscriptions_status_valid CHECK (
subscription_status IN ('active', 'suspended', 'terminated')
),
CONSTRAINT subscriptions_dates_valid CHECK (
activation_date <= first_payment_date
AND (end_date IS NULL OR end_date >= first_payment_date)
)
);
CREATE INDEX subscriptions_user_id_idx ON subscriptions(user_id);
CREATE INDEX subscriptions_first_payment_date_idx ON subscriptions(first_payment_date);
CREATE TABLE subscription_payments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
subscription_id uuid NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
renewal_date text NOT NULL,
status text NOT NULL,
payment_date text,
linked_payment_id uuid,
created_at text NOT NULL,
updated_at text NOT NULL,
UNIQUE (subscription_id, renewal_date, user_id)
);
CREATE INDEX subscription_payments_user_id_idx ON subscription_payments(user_id);
CREATE TABLE bills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
amount double precision NOT NULL,
due_date text NOT NULL,
status text NOT NULL,
payment_date text,
linked_payment_id uuid,
created_at text NOT NULL,
updated_at text NOT NULL
);
CREATE INDEX bills_user_id_idx ON bills(user_id);
CREATE INDEX bills_due_date_idx ON bills(due_date);
CREATE TABLE one_time_payments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
amount double precision NOT NULL,
date text NOT NULL,
linked_payment_id uuid,
created_at text NOT NULL,
updated_at text NOT NULL
);
CREATE INDEX one_time_payments_user_id_idx ON one_time_payments(user_id);
CREATE TABLE payments_ledger (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
amount double precision NOT NULL,
date text NOT NULL,
source_type text NOT NULL,
source_id uuid NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL,
UNIQUE (source_type, source_id, user_id)
);
CREATE INDEX payments_ledger_user_id_idx ON payments_ledger(user_id);
CREATE INDEX payments_ledger_date_idx ON payments_ledger(date);
CREATE TABLE payment_titles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name text NOT NULL,
normalized_name text NOT NULL,
payment_type text NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL,
CONSTRAINT payment_titles_type_valid CHECK (
payment_type IN ('subscription', 'bill', 'one_time', 'income', 'any')
),
UNIQUE (user_id, normalized_name, payment_type)
);
CREATE INDEX payment_titles_user_id_idx ON payment_titles(user_id);
CREATE TABLE income (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
amount double precision NOT NULL,
type text NOT NULL,
date text NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL
);
CREATE INDEX income_user_id_idx ON income(user_id);
CREATE TABLE user_settings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
full_name text,
monthly_income_goal double precision,
monthly_savings_goal double precision,
currency text NOT NULL,
locale text NOT NULL,
first_day_of_week text NOT NULL,
default_dashboard_view text NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL
);

View File

@@ -0,0 +1,104 @@
BEGIN;
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS activation_date text,
ADD COLUMN IF NOT EXISTS first_payment_date text,
ADD COLUMN IF NOT EXISTS end_date text,
ADD COLUMN IF NOT EXISTS occurrence_count integer,
ADD COLUMN IF NOT EXISTS custom_interval_days integer;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'subscriptions' AND column_name = 'renewal_date'
) THEN
UPDATE subscriptions
SET
activation_date = COALESCE(activation_date, renewal_date),
first_payment_date = COALESCE(first_payment_date, renewal_date),
subscription_status = CASE
WHEN subscription_status = 'disabled' THEN 'terminated'
ELSE subscription_status
END;
ELSE
UPDATE subscriptions
SET subscription_status = 'terminated'
WHERE subscription_status = 'disabled';
END IF;
END $$;
ALTER TABLE subscriptions
ALTER COLUMN activation_date SET NOT NULL,
ALTER COLUMN first_payment_date SET NOT NULL;
ALTER TABLE subscriptions
DROP COLUMN IF EXISTS renewal_date,
DROP COLUMN IF EXISTS auto_renew;
DROP INDEX IF EXISTS subscriptions_renewal_date_idx;
CREATE INDEX IF NOT EXISTS subscriptions_first_payment_date_idx
ON subscriptions(first_payment_date);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_occurrence_count_positive'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_occurrence_count_positive
CHECK (occurrence_count > 0);
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_custom_interval_days_positive'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_custom_interval_days_positive
CHECK (custom_interval_days > 0);
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_amount_positive'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_amount_positive CHECK (amount > 0);
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_billing_cycle_valid'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_billing_cycle_valid
CHECK (billing_cycle IN ('weekly', 'monthly', 'yearly', 'custom'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_status_valid'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_status_valid
CHECK (subscription_status IN ('active', 'suspended', 'terminated'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_dates_valid'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_dates_valid
CHECK (
activation_date <= first_payment_date
AND (end_date IS NULL OR end_date >= first_payment_date)
);
END IF;
END $$;
COMMIT;

View File

@@ -0,0 +1,49 @@
BEGIN;
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS custom_interval_value integer,
ADD COLUMN IF NOT EXISTS custom_interval_unit text;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'subscriptions' AND column_name = 'custom_interval_days'
) THEN
UPDATE subscriptions
SET
custom_interval_value = COALESCE(custom_interval_value, custom_interval_days),
custom_interval_unit = CASE
WHEN custom_interval_days IS NOT NULL THEN COALESCE(custom_interval_unit, 'days')
ELSE custom_interval_unit
END;
END IF;
END $$;
ALTER TABLE subscriptions
DROP COLUMN IF EXISTS custom_interval_days;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_custom_interval_value_positive'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_custom_interval_value_positive
CHECK (custom_interval_value > 0);
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'subscriptions_custom_interval_unit_valid'
) THEN
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_custom_interval_unit_valid
CHECK (custom_interval_unit IN ('days', 'weeks', 'months', 'years'));
END IF;
END $$;
COMMIT;

View File

@@ -0,0 +1,7 @@
UPDATE payments_ledger AS ledger
SET date = payment.renewal_date,
updated_at = NOW()
FROM subscription_payments AS payment
WHERE ledger.source_type = 'subscription'
AND ledger.source_id = payment.id
AND ledger.date IS DISTINCT FROM payment.renewal_date;

View File

@@ -0,0 +1,32 @@
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'payment_titles_type_valid'
) THEN
ALTER TABLE payment_titles
ADD CONSTRAINT payment_titles_type_valid CHECK (
payment_type IN ('subscription', 'bill', 'one_time', 'income', 'any')
);
END IF;
END $$;
INSERT INTO payment_titles (
user_id,
name,
normalized_name,
payment_type,
created_at,
updated_at
)
SELECT DISTINCT
user_id,
title,
LOWER(REGEXP_REPLACE(TRIM(title), '\s+', ' ', 'g')),
'income',
NOW()::text,
NOW()::text
FROM income
WHERE TRIM(title) <> ''
ON CONFLICT (user_id, normalized_name, payment_type) DO NOTHING;

View File

@@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN IF NOT EXISTS avatar_file text;