import { createHash, randomBytes, randomUUID } from "node:crypto"; import { and, eq } from "drizzle-orm"; import type { Database } from "@app/database"; import { schema } from "@app/database"; import type { EventType } from "@app/shared-types"; import type { NewDomainEvent } from "@app/events"; import { errors } from "./errors.js"; /** Slumpad, läsbar inbjudningskod utan lättförväxlade tecken. */ export function generateInviteCode(): string { const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const bytes = randomBytes(8); let code = ""; for (const b of bytes) code += alphabet[b % alphabet.length]; return code; } export function sha256(input: string): string { return createHash("sha256").update(input).digest("hex"); } export function todayIso(): string { return new Date().toISOString().slice(0, 10); } export function currentMonth(): string { return new Date().toISOString().slice(0, 7); } /** Verifiera hushållsmedlemskap – enda vägen till hushållsdata (spec §56). */ export async function requireMembership( db: Database, householdId: string, userId: string, ): Promise<{ role: string; portionFactor: number }> { const [member] = await db .select() .from(schema.householdMembers) .where( and( eq(schema.householdMembers.householdId, householdId), eq(schema.householdMembers.userId, userId), ), ) .limit(1); if (!member) throw errors.forbidden("Du är inte medlem i det här hushållet."); return { role: member.role, portionFactor: member.portionFactor }; } /** Skapa hushåll med standard-förvaringsplatser (spec §7–8). Återanvänds av * både POST /v1/households och onboarding auto-create. */ export async function createHouseholdWithDefaults( db: Database, { userId, name, size, }: { userId: string; name: string; size?: number; }, ): Promise<{ id: string; name: string }> { const [household] = await db .insert(schema.households) .values({ name, inviteCode: generateInviteCode(), size: size ?? null, }) .returning(); await db.insert(schema.householdMembers).values({ householdId: household!.id, userId, role: "owner", }); await db.insert(schema.storageLocations).values([ { householdId: household!.id, type: "fridge", name: "Kylen", sortOrder: 0 }, { householdId: household!.id, type: "freezer", name: "Frysen", sortOrder: 1 }, { householdId: household!.id, type: "pantry", name: "Skafferiet", sortOrder: 2 }, ]); return { id: household!.id, name: household!.name }; } /** Hämta användarens aktiva hushåll (första medlemskapet) eller null. */ export async function getActiveHouseholdId(db: Database, userId: string): Promise { const [member] = await db .select({ householdId: schema.householdMembers.householdId }) .from(schema.householdMembers) .where(eq(schema.householdMembers.userId, userId)) .orderBy(schema.householdMembers.joinedAt) .limit(1); return member?.householdId ?? null; } /** Som ovan men kastar 404 om användaren saknar hushåll. */ export async function requireActiveHousehold(db: Database, userId: string): Promise { const id = await getActiveHouseholdId(db, userId); if (!id) { throw errors.notFound("Du har inget hushåll ännu. Skapa ett under Hemma → Hushåll."); } return id; } /** Skriv domänhändelse till outboxen (spec §55) – i samma transaktion när tx skickas in. */ export async function emitEvent( db: Database, event: NewDomainEvent, ): Promise { await db.insert(schema.domainEvents).values({ type: event.type, userId: event.userId ?? null, householdId: event.householdId ?? null, payload: event.payload as Record, correlationId: event.correlationId ?? null, }); } /** Audit-logg (spec §56). */ export async function audit( db: Database, entry: { actorUserId?: string | undefined; actorType?: "user" | "admin" | "system" | "worker"; action: string; targetType?: string; targetId?: string; metadata?: Record; ip?: string | undefined; correlationId?: string | undefined; }, ): Promise { await db.insert(schema.auditLogs).values({ actorUserId: entry.actorUserId ?? null, actorType: entry.actorType ?? "user", action: entry.action, targetType: entry.targetType ?? null, targetId: entry.targetId ?? null, metadata: entry.metadata ?? null, ip: entry.ip ?? null, correlationId: entry.correlationId ?? null, }); } export function newCorrelationId(): string { return randomUUID(); }