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:
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