Production deployment setup + feature complete

- Dockerfile + deploy.sh for Hetzner server
- Email verification via Better Auth + Resend
- Invite code flow (6-digit OTP, generate/join)
- Settlement share percent fix (payer vs debtor)
- OCR scanner fixes (date display, retry, viewfinder)
- app.json icon/splash/adaptive-icon configured
- iOS deployment target 15.5 (ML Kit requirement)
- DB migration 0014: household_invitations table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
René Schober
2026-03-20 11:54:22 +01:00
parent 4e34270786
commit 9ddc7c6d7a
194 changed files with 55961 additions and 305 deletions

View File

@@ -5,6 +5,9 @@
".": {
"default": "./src/index.ts"
},
"./schema": {
"default": "./src/schema/index.ts"
},
"./*": {
"default": "./src/*.ts"
}

View File

@@ -4,3 +4,6 @@ import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";
export const db = drizzle(env.DATABASE_URL, { schema });
// Re-export commonly used Drizzle utilities so consumers don't need a separate drizzle-orm dep
export { eq, and, or, not, gt, gte, lt, lte, isNull, isNotNull, inArray, asc, desc, sql } from "drizzle-orm";

View File

@@ -0,0 +1,174 @@
CREATE TYPE "public"."budget_context_type" AS ENUM('vacation', 'project', 'event');--> statement-breakpoint
CREATE TYPE "public"."category_type" AS ENUM('income', 'expense');--> statement-breakpoint
CREATE TYPE "public"."subscription_plan" AS ENUM('free', 'pro', 'family');--> statement-breakpoint
CREATE TYPE "public"."subscription_status" AS ENUM('active', 'canceled', 'past_due');--> statement-breakpoint
CREATE TYPE "public"."transaction_type" AS ENUM('income', 'expense');--> statement-breakpoint
CREATE TABLE "budget_contexts" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"type" "budget_context_type" NOT NULL,
"total_budget" numeric(12, 2) NOT NULL,
"currency" text DEFAULT 'EUR' NOT NULL,
"start_date" date,
"end_date" date,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "categories" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"icon" text,
"color" text,
"type" "category_type" NOT NULL,
"is_default" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "households" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"owner_id" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "savings_goals" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"target_amount" numeric(12, 2) NOT NULL,
"current_amount" numeric(12, 2) DEFAULT '0' NOT NULL,
"target_date" date,
"allocation_percent" numeric(5, 2),
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "shopping_list_items" (
"id" text PRIMARY KEY NOT NULL,
"list_id" text NOT NULL,
"added_by_user_id" text NOT NULL,
"name" text NOT NULL,
"quantity" numeric(10, 2),
"unit" text,
"is_checked" boolean DEFAULT false NOT NULL,
"checked_by_user_id" text,
"checked_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "shopping_lists" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "subscription_plans" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"plan" "subscription_plan" DEFAULT 'free' NOT NULL,
"status" "subscription_status" DEFAULT 'active' NOT NULL,
"stripe_customer_id" text,
"stripe_subscription_id" text,
"current_period_start" timestamp,
"current_period_end" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "transactions" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"user_id" text NOT NULL,
"category_id" text,
"amount" numeric(12, 2) NOT NULL,
"currency" text DEFAULT 'EUR' NOT NULL,
"type" "transaction_type" NOT NULL,
"description" text,
"merchant" text,
"date" date NOT NULL,
"receipt_image_url" text,
"budget_context_id" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "budget_contexts" ADD CONSTRAINT "budget_contexts_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "categories" ADD CONSTRAINT "categories_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "households" ADD CONSTRAINT "households_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "savings_goals" ADD CONSTRAINT "savings_goals_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_list_id_shopping_lists_id_fk" FOREIGN KEY ("list_id") REFERENCES "public"."shopping_lists"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_added_by_user_id_user_id_fk" FOREIGN KEY ("added_by_user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_checked_by_user_id_user_id_fk" FOREIGN KEY ("checked_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_lists" ADD CONSTRAINT "shopping_lists_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "subscription_plans" ADD CONSTRAINT "subscription_plans_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_budget_context_id_budget_contexts_id_fk" FOREIGN KEY ("budget_context_id") REFERENCES "public"."budget_contexts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "budget_contexts_household_id_idx" ON "budget_contexts" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "categories_household_id_idx" ON "categories" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "savings_goals_household_id_idx" ON "savings_goals" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "shopping_list_items_list_id_idx" ON "shopping_list_items" USING btree ("list_id");--> statement-breakpoint
CREATE INDEX "shopping_lists_household_id_idx" ON "shopping_lists" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "subscription_plans_household_id_idx" ON "subscription_plans" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "transactions_household_id_idx" ON "transactions" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "transactions_user_id_idx" ON "transactions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "transactions_date_idx" ON "transactions" USING btree ("date");--> statement-breakpoint
CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");

View File

@@ -0,0 +1,39 @@
CREATE TABLE "invitation" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text DEFAULT 'pending' NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "member" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role" text DEFAULT 'member' NOT NULL,
"created_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"logo" text,
"created_at" timestamp NOT NULL,
"metadata" text,
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "active_organization_id" text;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "invitation_organizationId_idx" ON "invitation" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "invitation_email_idx" ON "invitation" USING btree ("email");--> statement-breakpoint
CREATE INDEX "member_organizationId_idx" ON "member" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "member_userId_idx" ON "member" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "organization_slug_uidx" ON "organization" USING btree ("slug");

View File

@@ -0,0 +1,70 @@
DO $$ BEGIN CREATE TYPE "public"."sync_operation" AS ENUM('create', 'update', 'delete'); EXCEPTION WHEN duplicate_object THEN null; END $$;--> statement-breakpoint
DO $$ BEGIN CREATE TYPE "public"."transaction_scope" AS ENUM('household', 'private', 'child'); EXCEPTION WHEN duplicate_object THEN null; END $$;--> statement-breakpoint
CREATE TABLE "children" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"color" text DEFAULT '#378ADD' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sync_queue" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"user_id" text NOT NULL,
"operation" "sync_operation" NOT NULL,
"table_name" text NOT NULL,
"payload" jsonb NOT NULL,
"attempts" numeric DEFAULT '0' NOT NULL,
"last_error" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "vacation_entries" (
"id" text PRIMARY KEY NOT NULL,
"vacation_id" text NOT NULL,
"created_by" text NOT NULL,
"category_id" text,
"amount" numeric(12, 2) NOT NULL,
"currency" text DEFAULT 'EUR' NOT NULL,
"description" text,
"date" date NOT NULL,
"synced_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "vacations" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"budget" numeric(12, 2),
"currency" text DEFAULT 'EUR' NOT NULL,
"starts_on" date,
"ends_on" date,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "budget_contexts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
DROP TABLE "budget_contexts" CASCADE;--> statement-breakpoint
ALTER TABLE "transactions" DROP CONSTRAINT IF EXISTS "transactions_budget_context_id_budget_contexts_id_fk";
--> statement-breakpoint
ALTER TABLE "transactions" ADD COLUMN "child_id" text;--> statement-breakpoint
ALTER TABLE "transactions" ADD COLUMN "scope" "transaction_scope" DEFAULT 'household' NOT NULL;--> statement-breakpoint
ALTER TABLE "transactions" ADD COLUMN "is_fixed" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "transactions" ADD COLUMN "is_carry_over" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "transactions" ADD COLUMN "synced_at" timestamp;--> statement-breakpoint
ALTER TABLE "children" ADD CONSTRAINT "children_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sync_queue" ADD CONSTRAINT "sync_queue_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sync_queue" ADD CONSTRAINT "sync_queue_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "vacation_entries" ADD CONSTRAINT "vacation_entries_vacation_id_vacations_id_fk" FOREIGN KEY ("vacation_id") REFERENCES "public"."vacations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "vacation_entries" ADD CONSTRAINT "vacation_entries_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "vacation_entries" ADD CONSTRAINT "vacation_entries_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "vacations" ADD CONSTRAINT "vacations_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "children_household_id_idx" ON "children" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "sync_queue_household_id_idx" ON "sync_queue" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "vacation_entries_vacation_id_idx" ON "vacation_entries" USING btree ("vacation_id");--> statement-breakpoint
CREATE INDEX "vacations_household_id_idx" ON "vacations" USING btree ("household_id");--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_child_id_children_id_fk" FOREIGN KEY ("child_id") REFERENCES "public"."children"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "transactions_scope_idx" ON "transactions" USING btree ("scope");--> statement-breakpoint
ALTER TABLE "transactions" DROP COLUMN "budget_context_id";--> statement-breakpoint
DROP TYPE "public"."budget_context_type";

View File

@@ -0,0 +1,29 @@
CREATE TABLE "debt_payments" (
"id" text PRIMARY KEY NOT NULL,
"debt_id" text NOT NULL,
"amount" numeric(12, 2) NOT NULL,
"date" date NOT NULL,
"note" text,
"linked_transaction_id" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "debts" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"user_id" text NOT NULL,
"label" text NOT NULL,
"creditor" text,
"total_amount" numeric(12, 2) NOT NULL,
"notes" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"closed_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "debt_payments" ADD CONSTRAINT "debt_payments_debt_id_debts_id_fk" FOREIGN KEY ("debt_id") REFERENCES "public"."debts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "debt_payments" ADD CONSTRAINT "debt_payments_linked_transaction_id_transactions_id_fk" FOREIGN KEY ("linked_transaction_id") REFERENCES "public"."transactions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "debts" ADD CONSTRAINT "debts_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "debts" ADD CONSTRAINT "debts_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "debt_payments_debt_id_idx" ON "debt_payments" USING btree ("debt_id");--> statement-breakpoint
CREATE INDEX "debts_household_id_idx" ON "debts" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "debts_user_id_idx" ON "debts" USING btree ("user_id");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "debts" ADD COLUMN "creditor_user_id" text;--> statement-breakpoint
ALTER TABLE "debts" ADD CONSTRAINT "debts_creditor_user_id_user_id_fk" FOREIGN KEY ("creditor_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;

View File

@@ -0,0 +1,45 @@
CREATE TABLE "fixed_costs" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"scope" "transaction_scope" DEFAULT 'household' NOT NULL,
"child_id" text,
"category_id" text,
"label" text NOT NULL,
"amount" numeric(12, 2) NOT NULL,
"type" "transaction_type" DEFAULT 'expense' NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "monthly_transfers" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"month" text NOT NULL,
"from_user_id" text NOT NULL,
"to_user_id" text NOT NULL,
"amount" numeric(12, 2) NOT NULL,
"note" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "transfer_line_items" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"label" text NOT NULL,
"amount" numeric(12, 2) NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "fixed_costs" ADD CONSTRAINT "fixed_costs_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "fixed_costs" ADD CONSTRAINT "fixed_costs_child_id_children_id_fk" FOREIGN KEY ("child_id") REFERENCES "public"."children"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "fixed_costs" ADD CONSTRAINT "fixed_costs_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "monthly_transfers" ADD CONSTRAINT "monthly_transfers_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "monthly_transfers" ADD CONSTRAINT "monthly_transfers_from_user_id_user_id_fk" FOREIGN KEY ("from_user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "monthly_transfers" ADD CONSTRAINT "monthly_transfers_to_user_id_user_id_fk" FOREIGN KEY ("to_user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transfer_line_items" ADD CONSTRAINT "transfer_line_items_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "fixed_costs_household_id_idx" ON "fixed_costs" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "fixed_costs_scope_idx" ON "fixed_costs" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "monthly_transfers_household_id_idx" ON "monthly_transfers" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "monthly_transfers_month_idx" ON "monthly_transfers" USING btree ("month");--> statement-breakpoint
CREATE INDEX "transfer_line_items_household_id_idx" ON "transfer_line_items" USING btree ("household_id");

View File

@@ -0,0 +1,16 @@
CREATE TABLE "household_settings" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"owner_name" text DEFAULT 'Ich' NOT NULL,
"partner_name" text DEFAULT 'Partner' NOT NULL,
"user_share_percent" numeric(5, 2) DEFAULT '50' NOT NULL,
"monthly_budget" numeric(12, 2) DEFAULT '400' NOT NULL,
"currency" text DEFAULT 'EUR' NOT NULL,
"split_child_costs" boolean DEFAULT true NOT NULL,
"onboarding_complete" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "household_settings_household_id_unique" UNIQUE("household_id")
);
--> statement-breakpoint
ALTER TABLE "household_settings" ADD CONSTRAINT "household_settings_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,17 @@
CREATE TABLE "month_status" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"month" text NOT NULL,
"status" text DEFAULT 'open' NOT NULL,
"closed_at" timestamp,
"closed_by" text,
"final_amount" numeric(12, 2),
"notes" text,
"final_transfer_id" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "month_status" ADD CONSTRAINT "month_status_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "month_status" ADD CONSTRAINT "month_status_closed_by_user_id_fk" FOREIGN KEY ("closed_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "month_status_household_id_idx" ON "month_status" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "month_status_month_idx" ON "month_status" USING btree ("month");

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX "month_status_household_month_unique" ON "month_status" USING btree ("household_id","month");

View File

@@ -0,0 +1 @@
ALTER TABLE "household_settings" ADD COLUMN "language" text DEFAULT 'auto' NOT NULL;

View File

@@ -0,0 +1,14 @@
CREATE TABLE "shopping_items" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"label" text NOT NULL,
"quantity" text,
"added_by" text NOT NULL,
"checked_by" text,
"checked_at" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "shopping_items" ADD CONSTRAINT "shopping_items_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "shopping_items_household_id_idx" ON "shopping_items" USING btree ("household_id");

View File

@@ -0,0 +1 @@
ALTER TABLE "household_settings" ADD COLUMN "payer_user_id" text;

View File

@@ -0,0 +1,31 @@
CREATE TABLE "trip_expenses" (
"id" text PRIMARY KEY NOT NULL,
"trip_id" text NOT NULL,
"household_id" text NOT NULL,
"label" text NOT NULL,
"amount" numeric(12, 2) NOT NULL,
"category" text DEFAULT 'sonstiges' NOT NULL,
"paid_by" text NOT NULL,
"date" text NOT NULL,
"note" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "trips" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"name" text NOT NULL,
"destination" text,
"budget" numeric(12, 2) NOT NULL,
"start_date" text NOT NULL,
"end_date" text NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "trip_expenses" ADD CONSTRAINT "trip_expenses_trip_id_trips_id_fk" FOREIGN KEY ("trip_id") REFERENCES "public"."trips"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "trips" ADD CONSTRAINT "trips_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "trip_expenses_trip_id_idx" ON "trip_expenses" USING btree ("trip_id");--> statement-breakpoint
CREATE INDEX "trip_expenses_household_id_idx" ON "trip_expenses" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "trips_household_id_idx" ON "trips" USING btree ("household_id");

View File

@@ -0,0 +1,4 @@
ALTER TABLE "trips" ADD COLUMN "settlement_from_user_id" text;--> statement-breakpoint
ALTER TABLE "trips" ADD COLUMN "settlement_to_user_id" text;--> statement-breakpoint
ALTER TABLE "trips" ADD COLUMN "settlement_amount" numeric(12, 2);--> statement-breakpoint
ALTER TABLE "trips" ADD COLUMN "settled_at" text;

View File

@@ -0,0 +1,11 @@
CREATE TABLE "household_invitations" (
"id" text PRIMARY KEY NOT NULL,
"household_id" text NOT NULL,
"code" text NOT NULL,
"created_by" text NOT NULL,
"expires_at" text NOT NULL,
"used_at" text,
"used_by" text,
"created_at" text NOT NULL,
CONSTRAINT "household_invitations_code_unique" UNIQUE("code")
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1773381800099,
"tag": "0000_overjoyed_stingray",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1773383654638,
"tag": "0001_tiresome_vector",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1773416364202,
"tag": "0002_flawless_sasquatch",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1773419350413,
"tag": "0003_chilly_the_order",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1773420670722,
"tag": "0004_silly_wiccan",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1773421166761,
"tag": "0005_absurd_hulk",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1773665770861,
"tag": "0006_smooth_shiver_man",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1773666811100,
"tag": "0007_tense_earthquake",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1773666865784,
"tag": "0008_public_rachel_grey",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1773676918621,
"tag": "0009_skinny_thing",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1773730950919,
"tag": "0010_redundant_mongu",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1773903012939,
"tag": "0011_luxuriant_selene",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1773903947726,
"tag": "0012_busy_vulture",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1773906551276,
"tag": "0013_dizzy_lionheart",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1773996772794,
"tag": "0014_nostalgic_baron_strucker",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,529 @@
import { relations } from "drizzle-orm";
import {
boolean,
date,
index,
integer,
jsonb,
numeric,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { user } from "./auth";
// Enums
export const subscriptionPlanEnum = pgEnum("subscription_plan", ["free", "pro", "family"]);
export const subscriptionStatusEnum = pgEnum("subscription_status", ["active", "canceled", "past_due"]);
export const categoryTypeEnum = pgEnum("category_type", ["income", "expense"]);
export const transactionTypeEnum = pgEnum("transaction_type", ["income", "expense"]);
export const transactionScopeEnum = pgEnum("transaction_scope", ["household", "private", "child"]);
export const syncOperationEnum = pgEnum("sync_operation", ["create", "update", "delete"]);
// households table (tenant)
export const households = pgTable("households", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
ownerId: text("owner_id").notNull().references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// subscription_plans table
export const subscriptionPlans = pgTable(
"subscription_plans",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
plan: subscriptionPlanEnum("plan").notNull().default("free"),
status: subscriptionStatusEnum("status").notNull().default("active"),
stripeCustomerId: text("stripe_customer_id"),
stripeSubscriptionId: text("stripe_subscription_id"),
currentPeriodStart: timestamp("current_period_start"),
currentPeriodEnd: timestamp("current_period_end"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
},
(table) => [index("subscription_plans_household_id_idx").on(table.householdId)],
);
// categories table
export const categories = pgTable(
"categories",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
icon: text("icon"),
color: text("color"),
type: categoryTypeEnum("type").notNull(),
isDefault: boolean("is_default").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("categories_household_id_idx").on(table.householdId)],
);
// children table
export const children = pgTable(
"children",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
color: text("color").notNull().default("#378ADD"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("children_household_id_idx").on(table.householdId)],
);
// transactions table
export const transactions = pgTable(
"transactions",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
categoryId: text("category_id").references(() => categories.id, { onDelete: "set null" }),
childId: text("child_id").references(() => children.id, { onDelete: "set null" }),
scope: transactionScopeEnum("scope").notNull().default("household"),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
currency: text("currency").notNull().default("EUR"),
type: transactionTypeEnum("type").notNull(),
isFixed: boolean("is_fixed").notNull().default(false),
isCarryOver: boolean("is_carry_over").notNull().default(false),
description: text("description"),
merchant: text("merchant"),
date: date("date").notNull(),
receiptImageUrl: text("receipt_image_url"),
syncedAt: timestamp("synced_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
},
(table) => [
index("transactions_household_id_idx").on(table.householdId),
index("transactions_user_id_idx").on(table.userId),
index("transactions_date_idx").on(table.date),
index("transactions_scope_idx").on(table.scope),
],
);
// vacations table (replaces budget_contexts)
export const vacations = pgTable(
"vacations",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
budget: numeric("budget", { precision: 12, scale: 2 }),
currency: text("currency").notNull().default("EUR"),
startsOn: date("starts_on"),
endsOn: date("ends_on"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("vacations_household_id_idx").on(table.householdId)],
);
// vacation_entries table
export const vacationEntries = pgTable(
"vacation_entries",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
vacationId: text("vacation_id").notNull().references(() => vacations.id, { onDelete: "cascade" }),
createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
categoryId: text("category_id").references(() => categories.id, { onDelete: "set null" }),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
currency: text("currency").notNull().default("EUR"),
description: text("description"),
date: date("date").notNull(),
syncedAt: timestamp("synced_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("vacation_entries_vacation_id_idx").on(table.vacationId)],
);
// shopping_lists table
export const shoppingLists = pgTable(
"shopping_lists",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("shopping_lists_household_id_idx").on(table.householdId)],
);
// shopping_list_items table
export const shoppingListItems = pgTable(
"shopping_list_items",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
listId: text("list_id").notNull().references(() => shoppingLists.id, { onDelete: "cascade" }),
addedByUserId: text("added_by_user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
name: text("name").notNull(),
quantity: numeric("quantity", { precision: 10, scale: 2 }),
unit: text("unit"),
isChecked: boolean("is_checked").notNull().default(false),
checkedByUserId: text("checked_by_user_id").references(() => user.id, { onDelete: "set null" }),
checkedAt: timestamp("checked_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
},
(table) => [index("shopping_list_items_list_id_idx").on(table.listId)],
);
// shopping_items table — flat, household-scoped real-time shopping list
export const shoppingItems = pgTable(
"shopping_items",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
label: text("label").notNull(),
quantity: text("quantity"),
addedBy: text("added_by").notNull(),
checkedBy: text("checked_by"),
checkedAt: text("checked_at"),
sortOrder: integer("sort_order").notNull().default(0),
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
},
(table) => [index("shopping_items_household_id_idx").on(table.householdId)],
);
// savings_goals table
export const savingsGoals = pgTable(
"savings_goals",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
targetAmount: numeric("target_amount", { precision: 12, scale: 2 }).notNull(),
currentAmount: numeric("current_amount", { precision: 12, scale: 2 }).notNull().default("0"),
targetDate: date("target_date"),
allocationPercent: numeric("allocation_percent", { precision: 5, scale: 2 }),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("savings_goals_household_id_idx").on(table.householdId)],
);
// fixed_costs table — recurring transaction templates
export const fixedCosts = pgTable(
"fixed_costs",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
scope: transactionScopeEnum("scope").notNull().default("household"),
childId: text("child_id").references(() => children.id, { onDelete: "set null" }),
categoryId: text("category_id").references(() => categories.id, { onDelete: "set null" }),
label: text("label").notNull(),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
type: transactionTypeEnum("type").notNull().default("expense"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("fixed_costs_household_id_idx").on(table.householdId),
index("fixed_costs_scope_idx").on(table.scope),
],
);
// monthly_transfers table — recorded money transfers between members
export const monthlyTransfers = pgTable(
"monthly_transfers",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
month: text("month").notNull(), // "YYYY-MM"
fromUserId: text("from_user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
toUserId: text("to_user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
note: text("note"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("monthly_transfers_household_id_idx").on(table.householdId),
index("monthly_transfers_month_idx").on(table.month),
],
);
// transfer_line_items table — fixed additions to settlement calculation
export const transferLineItems = pgTable(
"transfer_line_items",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
label: text("label").notNull(),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("transfer_line_items_household_id_idx").on(table.householdId)],
);
// month_status table
export const monthStatus = pgTable(
"month_status",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
month: text("month").notNull(), // 'YYYY-MM'
status: text("status", { enum: ["open", "closed"] }).notNull().default("open"),
closedAt: timestamp("closed_at"),
closedBy: text("closed_by").references(() => user.id, { onDelete: "set null" }),
finalAmount: numeric("final_amount", { precision: 12, scale: 2 }),
notes: text("notes"),
finalTransferId: text("final_transfer_id"), // references monthly_transfers.id (no FK to avoid circular)
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("month_status_household_id_idx").on(table.householdId),
index("month_status_month_idx").on(table.month),
uniqueIndex("month_status_household_month_unique").on(table.householdId, table.month),
],
);
// household_settings table
export const householdSettings = pgTable("household_settings", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().unique().references(() => households.id, { onDelete: "cascade" }),
ownerName: text("owner_name").notNull().default("Ich"),
partnerName: text("partner_name").notNull().default("Partner"),
userSharePercent: numeric("user_share_percent", { precision: 5, scale: 2 }).notNull().default("50"),
monthlyBudget: numeric("monthly_budget", { precision: 12, scale: 2 }).notNull().default("400"),
currency: text("currency").notNull().default("EUR"),
splitChildCosts: boolean("split_child_costs").notNull().default(true),
payerUserId: text("payer_user_id"),
onboardingComplete: boolean("onboarding_complete").notNull().default(false),
language: text("language").notNull().default("auto"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
});
// debts table
export const debts = pgTable(
"debts",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
creditorUserId: text("creditor_user_id").references(() => user.id, { onDelete: "set null" }),
label: text("label").notNull(),
creditor: text("creditor"),
totalAmount: numeric("total_amount", { precision: 12, scale: 2 }).notNull(),
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow().notNull(),
closedAt: timestamp("closed_at"),
},
(table) => [
index("debts_household_id_idx").on(table.householdId),
index("debts_user_id_idx").on(table.userId),
],
);
// debt_payments table
export const debtPayments = pgTable(
"debt_payments",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
debtId: text("debt_id").notNull().references(() => debts.id, { onDelete: "cascade" }),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
date: date("date").notNull(),
note: text("note"),
linkedTransactionId: text("linked_transaction_id").references(() => transactions.id, { onDelete: "set null" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("debt_payments_debt_id_idx").on(table.debtId)],
);
// sync_queue table (Phase 2 — offline support)
export const syncQueue = pgTable(
"sync_queue",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
operation: syncOperationEnum("operation").notNull(),
tableName: text("table_name").notNull(),
payload: jsonb("payload").notNull(),
attempts: numeric("attempts").notNull().default("0"),
lastError: text("last_error"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [index("sync_queue_household_id_idx").on(table.householdId)],
);
// trips table
export const trips = pgTable(
"trips",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
householdId: text("household_id").notNull().references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
destination: text("destination"),
budget: numeric("budget", { precision: 12, scale: 2 }).notNull(),
startDate: text("start_date").notNull(),
endDate: text("end_date").notNull(),
status: text("status").notNull().default("active"),
settlementFromUserId: text("settlement_from_user_id"),
settlementToUserId: text("settlement_to_user_id"),
settlementAmount: numeric("settlement_amount", { precision: 12, scale: 2 }),
settledAt: text("settled_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(),
},
(table) => [index("trips_household_id_idx").on(table.householdId)],
);
// trip_expenses table
export const tripExpenses = pgTable(
"trip_expenses",
{
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
tripId: text("trip_id").notNull().references(() => trips.id, { onDelete: "cascade" }),
householdId: text("household_id").notNull(),
label: text("label").notNull(),
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
category: text("category").notNull().default("sonstiges"),
paidBy: text("paid_by").notNull(),
date: text("date").notNull(),
note: text("note"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("trip_expenses_trip_id_idx").on(table.tripId),
index("trip_expenses_household_id_idx").on(table.householdId),
],
);
// household_invitations table — short-lived join codes
export const householdInvitations = pgTable("household_invitations", {
id: text("id").primaryKey(),
householdId: text("household_id").notNull(),
code: text("code").notNull().unique(),
createdBy: text("created_by").notNull(),
expiresAt: text("expires_at").notNull(),
usedAt: text("used_at"),
usedBy: text("used_by"),
createdAt: text("created_at").notNull(),
});
// Relations
export const householdsRelations = relations(households, ({ one, many }) => ({
owner: one(user, { fields: [households.ownerId], references: [user.id] }),
subscriptionPlan: one(subscriptionPlans, { fields: [households.id], references: [subscriptionPlans.householdId] }),
categories: many(categories),
children: many(children),
transactions: many(transactions),
vacations: many(vacations),
shoppingLists: many(shoppingLists),
savingsGoals: many(savingsGoals),
debts: many(debts),
fixedCosts: many(fixedCosts),
settings: one(householdSettings, { fields: [households.id], references: [householdSettings.householdId] }),
monthlyTransfers: many(monthlyTransfers),
transferLineItems: many(transferLineItems),
trips: many(trips),
}));
export const subscriptionPlansRelations = relations(subscriptionPlans, ({ one }) => ({
household: one(households, { fields: [subscriptionPlans.householdId], references: [households.id] }),
}));
export const householdSettingsRelations = relations(householdSettings, ({ one }) => ({
household: one(households, { fields: [householdSettings.householdId], references: [households.id] }),
}));
export const monthStatusRelations = relations(monthStatus, ({ one }) => ({
household: one(households, { fields: [monthStatus.householdId], references: [households.id] }),
closedByUser: one(user, { fields: [monthStatus.closedBy], references: [user.id] }),
}));
export const categoriesRelations = relations(categories, ({ one, many }) => ({
household: one(households, { fields: [categories.householdId], references: [households.id] }),
transactions: many(transactions),
vacationEntries: many(vacationEntries),
}));
export const childrenRelations = relations(children, ({ one, many }) => ({
household: one(households, { fields: [children.householdId], references: [households.id] }),
transactions: many(transactions),
}));
export const transactionsRelations = relations(transactions, ({ one }) => ({
household: one(households, { fields: [transactions.householdId], references: [households.id] }),
user: one(user, { fields: [transactions.userId], references: [user.id] }),
category: one(categories, { fields: [transactions.categoryId], references: [categories.id] }),
child: one(children, { fields: [transactions.childId], references: [children.id] }),
}));
export const vacationsRelations = relations(vacations, ({ one, many }) => ({
household: one(households, { fields: [vacations.householdId], references: [households.id] }),
entries: many(vacationEntries),
}));
export const vacationEntriesRelations = relations(vacationEntries, ({ one }) => ({
vacation: one(vacations, { fields: [vacationEntries.vacationId], references: [vacations.id] }),
createdByUser: one(user, { fields: [vacationEntries.createdBy], references: [user.id] }),
category: one(categories, { fields: [vacationEntries.categoryId], references: [categories.id] }),
}));
export const shoppingListsRelations = relations(shoppingLists, ({ one, many }) => ({
household: one(households, { fields: [shoppingLists.householdId], references: [households.id] }),
items: many(shoppingListItems),
}));
export const shoppingListItemsRelations = relations(shoppingListItems, ({ one }) => ({
list: one(shoppingLists, { fields: [shoppingListItems.listId], references: [shoppingLists.id] }),
addedByUser: one(user, { fields: [shoppingListItems.addedByUserId], references: [user.id] }),
checkedByUser: one(user, { fields: [shoppingListItems.checkedByUserId], references: [user.id] }),
}));
export const savingsGoalsRelations = relations(savingsGoals, ({ one }) => ({
household: one(households, { fields: [savingsGoals.householdId], references: [households.id] }),
}));
export const shoppingItemsRelations = relations(shoppingItems, ({ one }) => ({
household: one(households, { fields: [shoppingItems.householdId], references: [households.id] }),
}));
export const fixedCostsRelations = relations(fixedCosts, ({ one }) => ({
household: one(households, { fields: [fixedCosts.householdId], references: [households.id] }),
category: one(categories, { fields: [fixedCosts.categoryId], references: [categories.id] }),
child: one(children, { fields: [fixedCosts.childId], references: [children.id] }),
}));
export const monthlyTransfersRelations = relations(monthlyTransfers, ({ one }) => ({
household: one(households, { fields: [monthlyTransfers.householdId], references: [households.id] }),
fromUser: one(user, { fields: [monthlyTransfers.fromUserId], references: [user.id] }),
toUser: one(user, { fields: [monthlyTransfers.toUserId], references: [user.id] }),
}));
export const transferLineItemsRelations = relations(transferLineItems, ({ one }) => ({
household: one(households, { fields: [transferLineItems.householdId], references: [households.id] }),
}));
export const debtsRelations = relations(debts, ({ one, many }) => ({
household: one(households, { fields: [debts.householdId], references: [households.id] }),
user: one(user, { fields: [debts.userId], references: [user.id] }),
payments: many(debtPayments),
}));
export const debtPaymentsRelations = relations(debtPayments, ({ one }) => ({
debt: one(debts, { fields: [debtPayments.debtId], references: [debts.id] }),
linkedTransaction: one(transactions, { fields: [debtPayments.linkedTransactionId], references: [transactions.id] }),
}));
export const syncQueueRelations = relations(syncQueue, ({ one }) => ({
household: one(households, { fields: [syncQueue.householdId], references: [households.id] }),
user: one(user, { fields: [syncQueue.userId], references: [user.id] }),
}));
export const tripsRelations = relations(trips, ({ one, many }) => ({
household: one(households, { fields: [trips.householdId], references: [households.id] }),
expenses: many(tripExpenses),
}));
export const tripExpensesRelations = relations(tripExpenses, ({ one }) => ({
trip: one(trips, { fields: [tripExpenses.tripId], references: [trips.id] }),
}));

View File

@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
import {
boolean,
index,
pgTable,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
@@ -10,7 +17,7 @@ export const user = pgTable("user", {
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.$onUpdate(() => new Date())
.notNull(),
});
@@ -22,13 +29,15 @@ export const session = pgTable(
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.$onUpdate(() => new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
// Added by organization plugin — tracks active household
activeOrganizationId: text("active_organization_id"),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
@@ -51,7 +60,7 @@ export const account = pgTable(
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
@@ -67,15 +76,73 @@ export const verification = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
// Organization plugin tables — Household = Organization in Better Auth
export const organization = pgTable(
"organization",
{
id: text("id").primaryKey(),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
logo: text("logo"),
createdAt: timestamp("created_at").notNull(),
metadata: text("metadata"),
},
(table) => [uniqueIndex("organization_slug_uidx").on(table.slug)],
);
export const member = pgTable(
"member",
{
id: text("id").primaryKey(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: text("role").default("member").notNull(),
createdAt: timestamp("created_at").notNull(),
},
(table) => [
index("member_organizationId_idx").on(table.organizationId),
index("member_userId_idx").on(table.userId),
],
);
export const invitation = pgTable(
"invitation",
{
id: text("id").primaryKey(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
email: text("email").notNull(),
role: text("role"),
status: text("status").default("pending").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
inviterId: text("inviter_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [
index("invitation_organizationId_idx").on(table.organizationId),
index("invitation_email_idx").on(table.email),
],
);
// Relations
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
members: many(member),
invitations: many(invitation),
}));
export const sessionRelations = relations(session, ({ one }) => ({
@@ -91,3 +158,30 @@ export const accountRelations = relations(account, ({ one }) => ({
references: [user.id],
}),
}));
export const organizationRelations = relations(organization, ({ many }) => ({
members: many(member),
invitations: many(invitation),
}));
export const memberRelations = relations(member, ({ one }) => ({
organization: one(organization, {
fields: [member.organizationId],
references: [organization.id],
}),
user: one(user, {
fields: [member.userId],
references: [user.id],
}),
}));
export const invitationRelations = relations(invitation, ({ one }) => ({
organization: one(organization, {
fields: [invitation.organizationId],
references: [organization.id],
}),
user: one(user, {
fields: [invitation.inviterId],
references: [user.id],
}),
}));

View File

@@ -1,2 +1,2 @@
export * from "./auth";
export {};
export * from "./app";