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:
@@ -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";
|
||||
|
||||
174
packages/db/src/migrations/0000_overjoyed_stingray.sql
Normal file
174
packages/db/src/migrations/0000_overjoyed_stingray.sql
Normal 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");
|
||||
39
packages/db/src/migrations/0001_tiresome_vector.sql
Normal file
39
packages/db/src/migrations/0001_tiresome_vector.sql
Normal 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");
|
||||
70
packages/db/src/migrations/0002_flawless_sasquatch.sql
Normal file
70
packages/db/src/migrations/0002_flawless_sasquatch.sql
Normal 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";
|
||||
29
packages/db/src/migrations/0003_chilly_the_order.sql
Normal file
29
packages/db/src/migrations/0003_chilly_the_order.sql
Normal 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");
|
||||
2
packages/db/src/migrations/0004_silly_wiccan.sql
Normal file
2
packages/db/src/migrations/0004_silly_wiccan.sql
Normal 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;
|
||||
45
packages/db/src/migrations/0005_absurd_hulk.sql
Normal file
45
packages/db/src/migrations/0005_absurd_hulk.sql
Normal 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");
|
||||
16
packages/db/src/migrations/0006_smooth_shiver_man.sql
Normal file
16
packages/db/src/migrations/0006_smooth_shiver_man.sql
Normal 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;
|
||||
17
packages/db/src/migrations/0007_tense_earthquake.sql
Normal file
17
packages/db/src/migrations/0007_tense_earthquake.sql
Normal 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");
|
||||
1
packages/db/src/migrations/0008_public_rachel_grey.sql
Normal file
1
packages/db/src/migrations/0008_public_rachel_grey.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX "month_status_household_month_unique" ON "month_status" USING btree ("household_id","month");
|
||||
1
packages/db/src/migrations/0009_skinny_thing.sql
Normal file
1
packages/db/src/migrations/0009_skinny_thing.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "household_settings" ADD COLUMN "language" text DEFAULT 'auto' NOT NULL;
|
||||
14
packages/db/src/migrations/0010_redundant_mongu.sql
Normal file
14
packages/db/src/migrations/0010_redundant_mongu.sql
Normal 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");
|
||||
1
packages/db/src/migrations/0011_luxuriant_selene.sql
Normal file
1
packages/db/src/migrations/0011_luxuriant_selene.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "household_settings" ADD COLUMN "payer_user_id" text;
|
||||
31
packages/db/src/migrations/0012_busy_vulture.sql
Normal file
31
packages/db/src/migrations/0012_busy_vulture.sql
Normal 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");
|
||||
4
packages/db/src/migrations/0013_dizzy_lionheart.sql
Normal file
4
packages/db/src/migrations/0013_dizzy_lionheart.sql
Normal 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;
|
||||
11
packages/db/src/migrations/0014_nostalgic_baron_strucker.sql
Normal file
11
packages/db/src/migrations/0014_nostalgic_baron_strucker.sql
Normal 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")
|
||||
);
|
||||
1286
packages/db/src/migrations/meta/0000_snapshot.json
Normal file
1286
packages/db/src/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1588
packages/db/src/migrations/meta/0001_snapshot.json
Normal file
1588
packages/db/src/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1945
packages/db/src/migrations/meta/0002_snapshot.json
Normal file
1945
packages/db/src/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2170
packages/db/src/migrations/meta/0003_snapshot.json
Normal file
2170
packages/db/src/migrations/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2189
packages/db/src/migrations/meta/0004_snapshot.json
Normal file
2189
packages/db/src/migrations/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2553
packages/db/src/migrations/meta/0005_snapshot.json
Normal file
2553
packages/db/src/migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2663
packages/db/src/migrations/meta/0006_snapshot.json
Normal file
2663
packages/db/src/migrations/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2796
packages/db/src/migrations/meta/0007_snapshot.json
Normal file
2796
packages/db/src/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2817
packages/db/src/migrations/meta/0008_snapshot.json
Normal file
2817
packages/db/src/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2824
packages/db/src/migrations/meta/0009_snapshot.json
Normal file
2824
packages/db/src/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2922
packages/db/src/migrations/meta/0010_snapshot.json
Normal file
2922
packages/db/src/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2928
packages/db/src/migrations/meta/0011_snapshot.json
Normal file
2928
packages/db/src/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3154
packages/db/src/migrations/meta/0012_snapshot.json
Normal file
3154
packages/db/src/migrations/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3178
packages/db/src/migrations/meta/0013_snapshot.json
Normal file
3178
packages/db/src/migrations/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3247
packages/db/src/migrations/meta/0014_snapshot.json
Normal file
3247
packages/db/src/migrations/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
111
packages/db/src/migrations/meta/_journal.json
Normal file
111
packages/db/src/migrations/meta/_journal.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
529
packages/db/src/schema/app.ts
Normal file
529
packages/db/src/schema/app.ts
Normal 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] }),
|
||||
}));
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./auth";
|
||||
export {};
|
||||
export * from "./app";
|
||||
|
||||
Reference in New Issue
Block a user