import { sql } from "drizzle-orm"; import { pgTable, uuid, text, bigint, integer, boolean, timestamp, jsonb, primaryKey, uniqueIndex, inet, } from "drizzle-orm/pg-core"; export const operators = pgTable( "operators", { id: uuid("id").primaryKey().defaultRandom(), username: text("username").notNull(), passwordHash: text("password_hash"), displayName: text("display_name").notNull(), role: text("role").notNull().default("admin"), defaultTimezone: text("default_timezone").notNull().default("Asia/Kuala_Lumpur"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => ({ usernameUnique: uniqueIndex("operators_username_uq").on(sql`lower(${t.username})`), }), ); export const whatsappAccounts = pgTable( "whatsapp_accounts", { id: uuid("id").primaryKey().defaultRandom(), operatorId: uuid("operator_id").notNull().references(() => operators.id), label: text("label").notNull(), phoneNumber: text("phone_number"), status: text("status").notNull().default("pending"), lastConnectedAt: timestamp("last_connected_at", { withTimezone: true }), lastQrAt: timestamp("last_qr_at", { withTimezone: true }), lastQrPng: text("last_qr_png"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => ({ operatorLabelUnique: uniqueIndex("whatsapp_accounts_operator_label_uq").on(t.operatorId, t.label), }), ); export const whatsappGroups = pgTable( "whatsapp_groups", { id: uuid("id").primaryKey().defaultRandom(), accountId: uuid("account_id").notNull().references(() => whatsappAccounts.id, { onDelete: "cascade" }), waGroupJid: text("wa_group_jid").notNull(), name: text("name").notNull(), participantCount: integer("participant_count").notNull().default(0), isArchived: boolean("is_archived").notNull().default(false), lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => ({ accountJidUnique: uniqueIndex("whatsapp_groups_account_jid_uq").on(t.accountId, t.waGroupJid), }), ); export const mediaFiles = pgTable("media_files", { id: uuid("id").primaryKey().defaultRandom(), operatorId: uuid("operator_id").notNull().references(() => operators.id), filenameOriginal: text("filename_original").notNull(), mimeType: text("mime_type").notNull(), sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(), sha256: text("sha256").notNull(), storagePath: text("storage_path").notNull(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); export const reminders = pgTable("reminders", { id: uuid("id").primaryKey().defaultRandom(), accountId: uuid("account_id").notNull().references(() => whatsappAccounts.id, { onDelete: "cascade" }), name: text("name").notNull(), scheduleKind: text("schedule_kind").notNull(), scheduledAt: timestamp("scheduled_at", { withTimezone: true }), rrule: text("rrule"), timezone: text("timezone").notNull(), endsAt: timestamp("ends_at", { withTimezone: true }), maxRuns: integer("max_runs"), status: text("status").notNull().default("active"), createdBy: uuid("created_by").notNull().references(() => operators.id), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), lastFiredAt: timestamp("last_fired_at", { withTimezone: true }), // Delivery window (operator timezone). End hour is enforced at runtime // by fire-reminder when window enforcement lands; start hour is documented // here but not gated in v1. deliveryWindowStartHour: integer("delivery_window_start_hour").notNull().default(6), deliveryWindowEndHour: integer("delivery_window_end_hour").notNull().default(18), }); export const reminderTargets = pgTable( "reminder_targets", { reminderId: uuid("reminder_id").notNull().references(() => reminders.id, { onDelete: "cascade" }), groupId: uuid("group_id").notNull().references(() => whatsappGroups.id), position: integer("position").notNull().default(0), }, (t) => ({ pk: primaryKey({ columns: [t.reminderId, t.groupId] }), }), ); export const reminderMessages = pgTable("reminder_messages", { id: uuid("id").primaryKey().defaultRandom(), reminderId: uuid("reminder_id").notNull().references(() => reminders.id, { onDelete: "cascade" }), position: integer("position").notNull(), kind: text("kind").notNull(), textContent: text("text_content"), mediaId: uuid("media_id").references(() => mediaFiles.id), }); export const reminderRuns = pgTable("reminder_runs", { id: uuid("id").primaryKey().defaultRandom(), // Nullable + ON DELETE SET NULL: deleting a reminder must NOT erase its // run history. The accompanying snapshot fields below preserve enough // context to keep history rows readable. reminderId: uuid("reminder_id").references(() => reminders.id, { onDelete: "set null" }), reminderName: text("reminder_name"), firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(), status: text("status").notNull(), errorSummary: text("error_summary"), // Soft-archive: non-null hides the row from the default activity // listing but keeps it queryable under a dedicated "Archived" filter. // The user can restore (unarchive) later or hard-delete from there. archivedAt: timestamp("archived_at", { withTimezone: true }), }); export const reminderRunTargets = pgTable("reminder_run_targets", { id: uuid("id").primaryKey().defaultRandom(), runId: uuid("run_id").notNull().references(() => reminderRuns.id, { onDelete: "cascade" }), // Nullable + ON DELETE SET NULL: unpair/delete-account wipes the // associated whatsapp_groups rows but the historical fan-out record // ("we tried to send to this group, here's the result") must survive. // The accompanying snapshot field below preserves the readable label. groupId: uuid("group_id").references(() => whatsappGroups.id, { onDelete: "set null" }), groupLabel: text("group_label"), status: text("status").notNull(), waMessageId: text("wa_message_id"), error: text("error"), latencyMs: integer("latency_ms"), }); export const auditLog = pgTable("audit_log", { id: uuid("id").primaryKey().defaultRandom(), operatorId: uuid("operator_id").references(() => operators.id), source: text("source").notNull(), action: text("action").notNull(), targetType: text("target_type"), targetId: uuid("target_id"), payload: jsonb("payload").notNull().default({}), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); export const authSessions = pgTable("auth_sessions", { id: uuid("id").primaryKey().defaultRandom(), operatorId: uuid("operator_id").notNull().references(() => operators.id), tokenHash: text("token_hash").notNull().unique(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), lastUsedAt: timestamp("last_used_at", { withTimezone: true }).notNull().defaultNow(), ipAddress: inet("ip_address"), userAgent: text("user_agent"), }); export const cacheEntries = pgTable("cache_entries", { key: text("key").primaryKey(), value: jsonb("value").notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), }); export const rateLimitBuckets = pgTable("rate_limit_buckets", { key: text("key").primaryKey(), windowStart: timestamp("window_start", { withTimezone: true }).notNull(), count: integer("count").notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), }); export type Operator = typeof operators.$inferSelect; export type NewOperator = typeof operators.$inferInsert; export type WhatsappAccount = typeof whatsappAccounts.$inferSelect; export type NewWhatsappAccount = typeof whatsappAccounts.$inferInsert; export type WhatsappGroup = typeof whatsappGroups.$inferSelect; export type NewWhatsappGroup = typeof whatsappGroups.$inferInsert; export type AuditLogEntry = typeof auditLog.$inferSelect; export type NewAuditLogEntry = typeof auditLog.$inferInsert;