import { sqliteTable, text, integer, uniqueIndex, index, } from "drizzle-orm/sqlite-core"; // ---- better-auth çekirdek tabloları (+ kredi alanları) ---- export const user = sqliteTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: integer("email_verified", { mode: "boolean" }) .notNull() .default(false), image: text("image"), creditBalance: integer("credit_balance").notNull().default(0), hasPaket: integer("has_paket", { mode: "boolean" }).notNull().default(false), // Kredi-bitti hatırlatma durum makinesi (kuruldu → gönderildi): // ilk INSUFFICIENT anı; kredi yüklenince sıfırlanır (hatırlatma iptal). krediBittiAt: integer("kredi_bitti_at", { mode: "timestamp" }), // Hatırlatma e-postası bir kez gönderilir, bu alan hiç sıfırlanmaz. krediHatirlatmaGonderildiAt: integer("kredi_hatirlatma_gonderildi_at", { mode: "timestamp", }), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); export const session = sqliteTable("session", { id: text("id").primaryKey(), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), token: text("token").notNull().unique(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); export const account = sqliteTable("account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp", }), refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp", }), scope: text("scope"), password: text("password"), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); export const verification = sqliteTable("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }), updatedAt: integer("updated_at", { mode: "timestamp" }), }); // ---- Ürün tabloları ---- export const orders = sqliteTable( "orders", { // cuid — iyzico conversationId olarak da kullanılır id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => user.id), product: text("product", { enum: ["paket", "topup"] }).notNull(), amountKurus: integer("amount_kurus").notNull(), credits: integer("credits").notNull(), status: text("status", { enum: ["pending", "paid", "failed"] }) .notNull() .default("pending"), iyzicoToken: text("iyzico_token"), // Gömülü ödeme formu script'i (checkoutFormContent) — /odeme/[siparis] // sayfası yenilendiğinde yeniden initialize gerekmesin diye saklanır; // sipariş sonuçlanınca temizlenir. iyzicoFormContent: text("iyzico_form_content"), iyzicoPaymentId: text("iyzico_payment_id"), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), paidAt: integer("paid_at", { mode: "timestamp" }), }, (t) => [ index("orders_user").on(t.userId), // Callback ve webhook siparişi token'dan bulur; UNIQUE aynı zamanda bir // token'ın iki siparişe bağlanmasını da imkânsız kılar (SQLite'ta çoklu // NULL serbesttir, token'sız pending siparişler etkilenmez). uniqueIndex("orders_iyzico_token").on(t.iyzicoToken), ], ); export const creditLedger = sqliteTable( "credit_ledger", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => user.id), delta: integer("delta").notNull(), reason: text("reason", { enum: [ "trial_grant", "purchase", "topup", "chat_message", "refund", "report_generate", "report_revision", ], }).notNull(), refId: text("ref_id"), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), }, (t) => [ uniqueIndex("ledger_reason_ref").on(t.reason, t.refId), index("ledger_user").on(t.userId), ], ); export const chatMessages = sqliteTable( "chat_messages", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => user.id), role: text("role", { enum: ["user", "assistant"] }).notNull(), content: text("content").notNull(), clientMessageId: text("client_message_id").unique(), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), }, (t) => [index("chat_user_created").on(t.userId, t.createdAt)], ); export const reports = sqliteTable("reports", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .unique() .references(() => user.id), params: text("params", { mode: "json" }).notNull(), result: text("result", { mode: "json" }), revisionCount: integer("revision_count").notNull().default(0), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); // Kullanıcı başına üretim kilidi. `credit_ledger.ref_id` benzersizliği yalnız // AYNI denemenin tekrarını (ağda kaybolan cevap) koruyor; requestId istemcide // her mount'ta yeniden üretildiği için "aynı kullanıcı üretimi iki kez // başlattı" durumunu hiç yakalamıyordu: ikinci sekme ya da geri→ileri, ikinci // bir ListeUretici monte edip 3 krediyi ikinci kez yakıyordu (tek rapor, // çünkü reports.user_id benzersiz ve üstüne yazılıyor). // userId birincil anahtar → ikinci INSERT benzersizlik ihlaliyle düşer, bu da // "üretim sürüyor" demektir. startedAt bayat kilitleri (süreç öldü, konteyner // yeniden başladı) devralmak için var. export const reportLocks = sqliteTable("report_locks", { userId: text("user_id") .primaryKey() .references(() => user.id), /** Kilidi tutan deneme; salım yalnız sahibi tarafından yapılabilsin diye. */ requestId: text("request_id").notNull(), startedAt: integer("started_at", { mode: "timestamp" }).notNull(), }); // Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları. // Anahtar: sıra kovası × puan türü × kategori × üniversite tipi // ("genel" = kategorisiz/tipsiz fallback). Anonim istek LLM'e asla gitmez; // bu tablo salt-okunur servis edilir. // NOT: Yeni tablo/kolon eklerken `pnpm db:generate` ile migration üretilmeli; // prod şeması açılışta yalnızca drizzle/ altındaki migration'larla güncellenir. export const tadimlikHavuzu = sqliteTable( "tadimlik_havuzu", { id: text("id").primaryKey(), kovaSlug: text("kova_slug").notNull(), tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(), kategoriSlug: text("kategori_slug").notNull(), // Sihirbazdaki devlet/vakıf tercihine uyan varyant; "genel" = tip // filtresi olmayan satır (tipsiz kullanıcı bunu görür). tip: text("tip", { enum: ["genel", "devlet", "vakif"] }) .notNull() .default("genel"), programId: text("program_id").notNull(), dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(), gerekce: text("gerekce").notNull(), riskNotu: text("risk_notu").notNull(), trendOzeti: text("trend_ozeti").notNull(), // Üretim anındaki program snapshot'ı (isim, universite, il, unitur, // efektifSira, siraGecmisi) — render yokatlas sorgusu gerektirmesin diye. program: text("program", { mode: "json" }).notNull(), // Üretimde kullanılan temsili aday sırası (kovanın orta noktası). ornekSira: integer("ornek_sira").notNull(), uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(), }, (t) => [ uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug, t.tip), ], ); // Manuel liste + sihirbaz profili — girişte localStorage'dan merge edilir // (çakışmada client kazanır, 24 sınırı korunur). reports ile aynı upsert deseni. export const savedLists = sqliteTable("saved_lists", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .unique() .references(() => user.id), // ManuelTercih[] (≤24, sıra korunur) items: text("items", { mode: "json" }).notNull(), // TercihProfili | null profil: text("profil", { mode: "json" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), });