Production deployment setup + feature complete

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

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

View File

@@ -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"
}
}

View 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];

View 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>;

View 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>;

View 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>;

View 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>;

View 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>;

View 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>;

View 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>;

View 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>;

View 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>;

View 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" };

View 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>;

View 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>;

View 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";

View File

@@ -0,0 +1,11 @@
{
"extends": "@haushaltsApp/config/tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src"]
}