import { boolean, doublePrecision, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uuid, } from "drizzle-orm/pg-core"; import { createdAt, creatorLevelEnum, eventTypeEnum, notificationTypeEnum, profileVisibilityEnum, updatedAt, } from "./_shared.js"; import { households } from "./households.js"; import { users } from "./users.js"; /** * Domänhändelser (spec §55) i outbox-mönster: skrivs transaktionellt med * affärsdata och publiceras asynkront till konsumenter (analytics, memory, * recommendations, training). */ export const domainEvents = pgTable( "domain_events", { id: uuid("id").primaryKey().defaultRandom(), type: eventTypeEnum("type").notNull(), userId: uuid("user_id"), householdId: uuid("household_id"), payload: jsonb("payload").notNull(), correlationId: text("correlation_id"), occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(), publishedAt: timestamp("published_at", { withTimezone: true }), }, (t) => [ index("domain_events_unpublished_idx").on(t.publishedAt, t.occurredAt), index("domain_events_type_idx").on(t.type, t.occurredAt), index("domain_events_household_idx").on(t.householdId, t.occurredAt), ], ); export const featureFlags = pgTable("feature_flags", { key: text("key").primaryKey(), enabled: boolean("enabled").notNull().default(false), descriptionSv: text("description_sv"), rolloutPercent: integer("rollout_percent").notNull().default(100), updatedAt: updatedAt(), }); /** Audit log (spec §56): vem gjorde vad, när, mot vad. */ export const auditLogs = pgTable( "audit_logs", { id: uuid("id").primaryKey().defaultRandom(), actorUserId: uuid("actor_user_id"), actorType: text("actor_type").notNull().default("user"), action: text("action").notNull(), targetType: text("target_type"), targetId: text("target_id"), metadata: jsonb("metadata"), ip: text("ip"), correlationId: text("correlation_id"), createdAt: createdAt(), }, (t) => [ index("audit_logs_actor_idx").on(t.actorUserId, t.createdAt), index("audit_logs_action_idx").on(t.action, t.createdAt), ], ); /** Idempotency-nycklar för säkra POST-retries (spec §56, §59). */ export const idempotencyKeys = pgTable( "idempotency_keys", { userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), key: text("key").notNull(), endpoint: text("endpoint").notNull(), responseStatus: integer("response_status"), responseBody: jsonb("response_body"), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.userId, t.key, t.endpoint] })], ); /** Notiser (spec §40). */ export const notifications = pgTable( "notifications", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), type: notificationTypeEnum("type").notNull(), titleSv: text("title_sv").notNull(), bodySv: text("body_sv").notNull(), data: jsonb("data"), /** i18n-spec §26: mall + variabler; titleSv/bodySv är renderad cache. */ templateKey: text("template_key"), variables: jsonb("variables"), locale: text("locale"), scheduledFor: timestamp("scheduled_for", { withTimezone: true }), sentAt: timestamp("sent_at", { withTimezone: true }), readAt: timestamp("read_at", { withTimezone: true }), createdAt: createdAt(), }, (t) => [index("notifications_user_idx").on(t.userId, t.createdAt)], ); /** Push-tokens för Expo push. */ export const pushTokens = pgTable( "push_tokens", { userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), token: text("token").notNull(), platform: text("platform").notNull(), updatedAt: updatedAt(), }, (t) => [primaryKey({ columns: [t.userId, t.token] })], ); /** Creator-statistik (spec §37). */ export const creatorStats = pgTable("creator_stats", { userId: uuid("user_id") .primaryKey() .references(() => users.id, { onDelete: "cascade" }), visibility: profileVisibilityEnum("visibility").notNull().default("private"), level: creatorLevelEnum("level").notNull().default("beginner"), publishedRecipes: integer("published_recipes").notNull().default(0), followers: integer("followers").notNull().default(0), totalCooks: integer("total_cooks").notNull().default(0), totalFavorites: integer("total_favorites").notNull().default(0), averageRating: doublePrecision("average_rating"), verifiedRecipes: integer("verified_recipes").notNull().default(0), badges: text("badges").array().notNull().default([]), updatedAt: updatedAt(), }); export const creatorFollows = pgTable( "creator_follows", { followerUserId: uuid("follower_user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), creatorUserId: uuid("creator_user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.followerUserId, t.creatorUserId] })], ); /** AI-evals (spec §34): fast testbibliotek + körningar. Ingen modelländring utan eval. */ export const aiEvalCases = pgTable("ai_eval_cases", { id: uuid("id").primaryKey().defaultRandom(), taskType: text("task_type").notNull(), nameSv: text("name_sv").notNull(), inputRef: jsonb("input_ref").notNull(), expectedOutput: jsonb("expected_output").notNull(), category: text("category").notNull().default("standard"), active: boolean("active").notNull().default(true), createdAt: createdAt(), }); export const aiEvalRuns = pgTable( "ai_eval_runs", { id: uuid("id").primaryKey().defaultRandom(), taskType: text("task_type").notNull(), modelVersion: text("model_version").notNull(), promptVersion: text("prompt_version").notNull(), metrics: jsonb("metrics").notNull(), passed: boolean("passed").notNull(), notes: text("notes"), createdAt: createdAt(), }, (t) => [index("ai_eval_runs_task_idx").on(t.taskType, t.createdAt)], ); /** Householdsvinn per vecka – aggregat för budget/matsvinnsvyer (spec §26). */ export const wasteSummaries = pgTable( "waste_summaries", { householdId: uuid("household_id") .notNull() .references(() => households.id, { onDelete: "cascade" }), weekStartDate: text("week_start_date").notNull(), discardedCount: integer("discarded_count").notNull().default(0), discardedValueMinor: integer("discarded_value_minor").notNull().default(0), updatedAt: updatedAt(), }, (t) => [primaryKey({ columns: [t.householdId, t.weekStartDate] })], );