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:
@@ -14,8 +14,10 @@
|
||||
"@better-auth/expo": "catalog:",
|
||||
"@haushaltsApp/db": "workspace:*",
|
||||
"@haushaltsApp/env": "workspace:*",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"better-auth": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"nodemailer": "^8.0.2",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,11 +4,23 @@ import * as schema from "@haushaltsApp/db/schema/auth";
|
||||
import { env } from "@haushaltsApp/env/server";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { bearer, organization } from "better-auth/plugins";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
function createTransport() {
|
||||
return nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: env.SMTP_PORT === 465,
|
||||
...(env.SMTP_USER && env.SMTP_PASSWORD
|
||||
? { auth: { user: env.SMTP_USER, pass: env.SMTP_PASSWORD } }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
|
||||
schema: schema,
|
||||
}),
|
||||
trustedOrigins: [
|
||||
@@ -20,7 +32,71 @@ export const auth = betterAuth({
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
async sendResetPassword(data) {
|
||||
await createTransport().sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to: data.user.email,
|
||||
subject: "Passwort zurücksetzen – HausApp",
|
||||
html: `
|
||||
<p>Hallo ${data.user.name ?? ""},</p>
|
||||
<p>Klicke auf den Link, um dein Passwort zurückzusetzen:</p>
|
||||
<p><a href="${data.url}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">Passwort zurücksetzen</a></p>
|
||||
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${data.url}</p>
|
||||
<p style="color:#9ca3af;font-size:12px;">Der Link ist 1 Stunde gültig.</p>
|
||||
`,
|
||||
});
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async (data) => {
|
||||
await createTransport().sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to: data.user.email,
|
||||
subject: "E-Mail bestätigen – HausApp",
|
||||
html: `
|
||||
<p>Hallo ${data.user.name ?? ""},</p>
|
||||
<p>Bitte bestätige deine E-Mail-Adresse:</p>
|
||||
<p><a href="${data.url}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">E-Mail bestätigen</a></p>
|
||||
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${data.url}</p>
|
||||
<p style="color:#9ca3af;font-size:12px;">Der Link ist 1 Stunde gültig.</p>
|
||||
`,
|
||||
});
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
...(env.APPLE_CLIENT_ID && env.APPLE_PRIVATE_KEY
|
||||
? {
|
||||
apple: {
|
||||
clientId: env.APPLE_CLIENT_ID,
|
||||
clientSecret: env.APPLE_PRIVATE_KEY,
|
||||
appBundleIdentifier: env.APPLE_CLIENT_ID,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
plugins: [
|
||||
expo(),
|
||||
bearer(),
|
||||
organization({
|
||||
allowUserToCreateOrganization: true,
|
||||
async sendInvitationEmail(data) {
|
||||
const inviteUrl = `${env.MOBILE_APP_SCHEME}invite?invitationId=${data.invitation.id}`;
|
||||
await createTransport().sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to: data.email,
|
||||
subject: `Einladung zu ${data.organization.name} – HausApp`,
|
||||
html: `
|
||||
<p>Hallo,</p>
|
||||
<p><strong>${data.inviter.user.name}</strong> hat dich eingeladen, dem Haushalt <strong>${data.organization.name}</strong> beizutreten.</p>
|
||||
<p>Klicke auf den Link, um die Einladung anzunehmen:</p>
|
||||
<p><a href="${inviteUrl}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">Einladung annehmen</a></p>
|
||||
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${inviteUrl}</p>
|
||||
`,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
baseURL: env.BETTER_AUTH_URL,
|
||||
advanced: {
|
||||
@@ -30,5 +106,4 @@ export const auth = betterAuth({
|
||||
httpOnly: true,
|
||||
},
|
||||
},
|
||||
plugins: [expo()],
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
".": {
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./schema": {
|
||||
"default": "./src/schema/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"default": "./src/*.ts"
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
10
packages/env/src/server.ts
vendored
10
packages/env/src/server.ts
vendored
@@ -9,6 +9,16 @@ export const env = createEnv({
|
||||
BETTER_AUTH_URL: z.url(),
|
||||
CORS_ORIGIN: z.url(),
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||
APPLE_CLIENT_ID: z.string().optional(),
|
||||
APPLE_TEAM_ID: z.string().optional(),
|
||||
APPLE_KEY_ID: z.string().optional(),
|
||||
APPLE_PRIVATE_KEY: z.string().optional(),
|
||||
MOBILE_APP_SCHEME: z.string().default("haushaltsApp://"),
|
||||
SMTP_HOST: z.string().default("localhost"),
|
||||
SMTP_PORT: z.coerce.number().default(1025),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_PASSWORD: z.string().optional(),
|
||||
SMTP_FROM: z.string().default("noreply@haushaltsapp.local"),
|
||||
},
|
||||
runtimeEnv: process.env,
|
||||
emptyStringAsUndefined: true,
|
||||
|
||||
24
packages/shared/package.json
Normal file
24
packages/shared/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@haushaltsApp/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./schemas/transaction": {
|
||||
"default": "./src/schemas/transaction.schema.ts"
|
||||
},
|
||||
"./schemas/trips": {
|
||||
"default": "./src/schemas/trips.schema.ts"
|
||||
},
|
||||
"./schemas/*": "./src/schemas/*.ts",
|
||||
"./types": "./src/types/index.ts",
|
||||
"./constants/*": "./src/constants/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@haushaltsApp/config": "workspace:*",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
23
packages/shared/src/constants/plans.ts
Normal file
23
packages/shared/src/constants/plans.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const PLAN_FEATURES = {
|
||||
free: {
|
||||
maxHouseholdMembers: 2,
|
||||
ocr: false,
|
||||
vacationBudgets: 1,
|
||||
realtimeSync: false,
|
||||
},
|
||||
pro: {
|
||||
maxHouseholdMembers: 5,
|
||||
ocr: true,
|
||||
vacationBudgets: 999,
|
||||
realtimeSync: true,
|
||||
},
|
||||
family: {
|
||||
maxHouseholdMembers: 999,
|
||||
ocr: true,
|
||||
vacationBudgets: 999,
|
||||
realtimeSync: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PlanType = keyof typeof PLAN_FEATURES;
|
||||
export type PlanFeatures = (typeof PLAN_FEATURES)[PlanType];
|
||||
15
packages/shared/src/schemas/auth.schema.ts
Normal file
15
packages/shared/src/schemas/auth.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const signUpSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8).max(100),
|
||||
});
|
||||
|
||||
export const signInSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export type SignUpInput = z.infer<typeof signUpSchema>;
|
||||
export type SignInInput = z.infer<typeof signInSchema>;
|
||||
11
packages/shared/src/schemas/children.schema.ts
Normal file
11
packages/shared/src/schemas/children.schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateChildSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#378ADD"),
|
||||
});
|
||||
|
||||
export const UpdateChildSchema = CreateChildSchema.partial();
|
||||
|
||||
export type CreateChildInput = z.infer<typeof CreateChildSchema>;
|
||||
export type UpdateChildInput = z.infer<typeof UpdateChildSchema>;
|
||||
19
packages/shared/src/schemas/debt.schema.ts
Normal file
19
packages/shared/src/schemas/debt.schema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateDebtSchema = z.object({
|
||||
label: z.string().min(1).max(255),
|
||||
creditorUserId: z.string().min(1).optional(), // internal household member
|
||||
creditor: z.string().max(255).optional(), // free-text fallback
|
||||
totalAmount: z.number().positive("Betrag muss positiv sein"),
|
||||
notes: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
export const CreateDebtPaymentSchema = z.object({
|
||||
debtId: z.string().min(1),
|
||||
amount: z.number().positive("Betrag muss positiv sein"),
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
note: z.string().max(255).optional(),
|
||||
});
|
||||
|
||||
export type CreateDebtInput = z.infer<typeof CreateDebtSchema>;
|
||||
export type CreateDebtPaymentInput = z.infer<typeof CreateDebtPaymentSchema>;
|
||||
41
packages/shared/src/schemas/fixed-costs.schema.ts
Normal file
41
packages/shared/src/schemas/fixed-costs.schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateFixedCostSchema = z.object({
|
||||
scope: z.enum(["household", "private", "child"]).default("household"),
|
||||
childId: z.string().min(1).optional(),
|
||||
categoryId: z.string().min(1).optional(),
|
||||
label: z.string().min(1).max(255),
|
||||
amount: z.number().positive(),
|
||||
type: z.enum(["income", "expense"]).default("expense"),
|
||||
});
|
||||
|
||||
export const UpdateFixedCostSchema = z.object({
|
||||
label: z.string().min(1).max(255).optional(),
|
||||
amount: z.number().positive().optional(),
|
||||
categoryId: z.string().min(1).nullable().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const CreateTransferLineItemSchema = z.object({
|
||||
label: z.string().min(1).max(255),
|
||||
amount: z.number().positive(),
|
||||
});
|
||||
|
||||
export const UpdateTransferLineItemSchema = z.object({
|
||||
label: z.string().min(1).max(255).optional(),
|
||||
amount: z.number().positive().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const CreateMonthlyTransferSchema = z.object({
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
toUserId: z.string().min(1),
|
||||
amount: z.number().positive(),
|
||||
note: z.string().max(255).optional(),
|
||||
});
|
||||
|
||||
export type CreateFixedCostInput = z.infer<typeof CreateFixedCostSchema>;
|
||||
export type UpdateFixedCostInput = z.infer<typeof UpdateFixedCostSchema>;
|
||||
export type CreateTransferLineItemInput = z.infer<typeof CreateTransferLineItemSchema>;
|
||||
export type UpdateTransferLineItemInput = z.infer<typeof UpdateTransferLineItemSchema>;
|
||||
export type CreateMonthlyTransferInput = z.infer<typeof CreateMonthlyTransferSchema>;
|
||||
15
packages/shared/src/schemas/household-settings.schema.ts
Normal file
15
packages/shared/src/schemas/household-settings.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const UpdateHouseholdSettingsSchema = z.object({
|
||||
ownerName: z.string().min(1).max(50).optional(),
|
||||
partnerName: z.string().min(1).max(50).optional(),
|
||||
userSharePercent: z.number().min(10).max(100).optional(),
|
||||
monthlyBudget: z.number().min(0).optional(),
|
||||
currency: z.string().length(3).optional(),
|
||||
splitChildCosts: z.boolean().optional(),
|
||||
payerUserId: z.string().nullable().optional(),
|
||||
onboardingComplete: z.boolean().optional(),
|
||||
language: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpdateHouseholdSettingsInput = z.infer<typeof UpdateHouseholdSettingsSchema>;
|
||||
10
packages/shared/src/schemas/household.schema.ts
Normal file
10
packages/shared/src/schemas/household.schema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createHouseholdSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export const updateHouseholdSchema = createHouseholdSchema.partial();
|
||||
|
||||
export type CreateHouseholdInput = z.infer<typeof createHouseholdSchema>;
|
||||
export type UpdateHouseholdInput = z.infer<typeof updateHouseholdSchema>;
|
||||
7
packages/shared/src/schemas/invite.schema.ts
Normal file
7
packages/shared/src/schemas/invite.schema.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const joinWithCodeSchema = z.object({
|
||||
code: z.string().length(6),
|
||||
});
|
||||
|
||||
export type JoinWithCodeInput = z.infer<typeof joinWithCodeSchema>;
|
||||
8
packages/shared/src/schemas/scanner.schema.ts
Normal file
8
packages/shared/src/schemas/scanner.schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const scanReceiptSchema = z.object({
|
||||
imageBase64: z.string().min(1),
|
||||
mimeType: z.enum(["image/jpeg", "image/png"]).default("image/jpeg"),
|
||||
});
|
||||
|
||||
export type ScanReceiptInput = z.infer<typeof scanReceiptSchema>;
|
||||
24
packages/shared/src/schemas/shopping-list.schema.ts
Normal file
24
packages/shared/src/schemas/shopping-list.schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createShoppingListSchema = z.object({
|
||||
householdId: z.string().min(1),
|
||||
name: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export const createShoppingListItemSchema = z.object({
|
||||
listId: z.string().min(1),
|
||||
name: z.string().min(1).max(200),
|
||||
quantity: z.string().optional(),
|
||||
unit: z.string().max(20).optional(),
|
||||
});
|
||||
|
||||
export const updateShoppingListItemSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
quantity: z.string().optional(),
|
||||
unit: z.string().max(20).optional(),
|
||||
isChecked: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateShoppingListInput = z.infer<typeof createShoppingListSchema>;
|
||||
export type CreateShoppingListItemInput = z.infer<typeof createShoppingListItemSchema>;
|
||||
export type UpdateShoppingListItemInput = z.infer<typeof updateShoppingListItemSchema>;
|
||||
39
packages/shared/src/schemas/shopping.schema.ts
Normal file
39
packages/shared/src/schemas/shopping.schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const shoppingItemSchema = z.object({
|
||||
id: z.string(),
|
||||
householdId: z.string(),
|
||||
label: z.string(),
|
||||
quantity: z.string().nullable(),
|
||||
addedBy: z.string(),
|
||||
checkedBy: z.string().nullable(),
|
||||
checkedAt: z.string().nullable(),
|
||||
sortOrder: z.number(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export type ShoppingItem = z.infer<typeof shoppingItemSchema>;
|
||||
|
||||
export const addShoppingItemSchema = z.object({
|
||||
label: z.string().min(1),
|
||||
quantity: z.string().optional(),
|
||||
});
|
||||
|
||||
export type AddShoppingItemInput = z.infer<typeof addShoppingItemSchema>;
|
||||
|
||||
// WebSocket event types sent from server → client
|
||||
export type ShoppingServerEvent =
|
||||
| { type: "item:added"; item: ShoppingItem }
|
||||
| { type: "item:checked"; itemId: string; checkedBy: string; checkedAt: string }
|
||||
| { type: "item:unchecked"; itemId: string }
|
||||
| { type: "item:deleted"; itemId: string }
|
||||
| { type: "item:cleared" }
|
||||
| { type: "sync"; items: ShoppingItem[] };
|
||||
|
||||
// WebSocket command types sent from client → server
|
||||
export type ShoppingClientCommand =
|
||||
| { type: "item:add"; label: string; quantity?: string }
|
||||
| { type: "item:check"; itemId: string }
|
||||
| { type: "item:uncheck"; itemId: string }
|
||||
| { type: "item:delete"; itemId: string }
|
||||
| { type: "item:clear" };
|
||||
32
packages/shared/src/schemas/transaction.schema.ts
Normal file
32
packages/shared/src/schemas/transaction.schema.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TransactionScopeSchema = z.enum(["household", "private", "child"]);
|
||||
|
||||
export const CreateTransactionSchema = z.object({
|
||||
amount: z.number().positive("Betrag muss positiv sein"),
|
||||
type: z.enum(["income", "expense"]),
|
||||
scope: TransactionScopeSchema.default("household"),
|
||||
childId: z.string().min(1).optional(),
|
||||
categoryId: z.string().min(1).optional(),
|
||||
description: z.string().max(255).optional(),
|
||||
merchant: z.string().max(255).optional(),
|
||||
date: z.string().datetime({ offset: true }),
|
||||
isFixed: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const UpdateTransactionSchema = CreateTransactionSchema.partial();
|
||||
|
||||
export const TransactionFiltersSchema = z.object({
|
||||
categoryId: z.string().optional(),
|
||||
type: z.enum(["income", "expense"]).optional(),
|
||||
scope: TransactionScopeSchema.optional(),
|
||||
childId: z.string().optional(),
|
||||
from: z.string().datetime({ offset: true }).optional(),
|
||||
to: z.string().datetime({ offset: true }).optional(),
|
||||
limit: z.coerce.number().min(1).max(100).default(50),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export type CreateTransactionInput = z.infer<typeof CreateTransactionSchema>;
|
||||
export type UpdateTransactionInput = z.infer<typeof UpdateTransactionSchema>;
|
||||
export type TransactionFilters = z.infer<typeof TransactionFiltersSchema>;
|
||||
30
packages/shared/src/schemas/trips.schema.ts
Normal file
30
packages/shared/src/schemas/trips.schema.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TRIP_CATEGORIES = ["unterkunft", "essen", "transport", "aktivitaeten", "sonstiges"] as const;
|
||||
export type TripCategory = (typeof TRIP_CATEGORIES)[number];
|
||||
|
||||
export const createTripSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
destination: z.string().max(200).optional(),
|
||||
budget: z.number().positive(),
|
||||
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
});
|
||||
|
||||
export const updateTripSchema = createTripSchema.partial();
|
||||
|
||||
export const createTripExpenseSchema = z.object({
|
||||
label: z.string().min(1).max(200),
|
||||
amount: z.number().positive(),
|
||||
category: z.enum(TRIP_CATEGORIES).default("sonstiges"),
|
||||
paidBy: z.string().min(1),
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
note: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export const updateTripExpenseSchema = createTripExpenseSchema.partial();
|
||||
|
||||
export type CreateTripInput = z.infer<typeof createTripSchema>;
|
||||
export type UpdateTripInput = z.infer<typeof updateTripSchema>;
|
||||
export type CreateTripExpenseInput = z.infer<typeof createTripExpenseSchema>;
|
||||
export type UpdateTripExpenseInput = z.infer<typeof updateTripExpenseSchema>;
|
||||
15
packages/shared/src/types/index.ts
Normal file
15
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type { SignUpInput, SignInInput } from "../schemas/auth.schema";
|
||||
export type { CreateHouseholdInput, UpdateHouseholdInput } from "../schemas/household.schema";
|
||||
export type { CreateTransactionInput, UpdateTransactionInput, TransactionFilters } from "../schemas/transaction.schema";
|
||||
export type { CreateChildInput, UpdateChildInput } from "../schemas/children.schema";
|
||||
export type {
|
||||
CreateShoppingListInput,
|
||||
CreateShoppingListItemInput,
|
||||
UpdateShoppingListItemInput,
|
||||
} from "../schemas/shopping-list.schema";
|
||||
export type {
|
||||
ShoppingItem,
|
||||
AddShoppingItemInput,
|
||||
ShoppingServerEvent,
|
||||
ShoppingClientCommand,
|
||||
} from "../schemas/shopping.schema";
|
||||
11
packages/shared/tsconfig.json
Normal file
11
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@haushaltsApp/config/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user