feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle

- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası)
- iyzico ödeme entegrasyonu ve paket satın alma akışı
- kredi sistemi ve uygulama veritabanı şeması (drizzle)
- AI destekli rapor üretimi ve tercih sihirbazı
- rapor yazdırma sayfası ve site header/kullanıcı menüsü

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bilalgursen
2026-07-22 02:13:39 +03:00
parent 7c3f17b9b5
commit cee6200237
64 changed files with 7966 additions and 89 deletions

252
src/app/sonuc/actions.ts Normal file
View File

@@ -0,0 +1,252 @@
"use server";
import { eq, sql } from "drizzle-orm";
import { getSession, getCurrentUser } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { PUAN_TURLERI, type PuanTuruKey } from "@/lib/db";
import { spendCredits, grantCredits, RAPOR_KREDI } from "@/lib/credits";
import {
raporUret,
listeOzetiCikar,
RaporUretimHatasi,
type RaporSonuc,
} from "@/lib/ai/rapor";
import type { RaporParams } from "@/lib/rapor-havuzu";
import { sihirbazDogrula, type SihirbazSecimleri } from "@/lib/sihirbaz";
const MAX_REVIZYON = 2;
// Modal yerinde render ettiği için action redirect etmez; sonucu döner.
export type ListeSonuc =
| {
ok: true;
rapor: RaporSonuc;
params: RaporParams;
revisionCount: number;
kredi: number;
}
| { ok: false; error: string; code: "AUTH" | "PAKET" | "KREDI" | "HATA" };
function siraTurGecerli(sira: number, tur: string): tur is PuanTuruKey {
return (
Number.isFinite(sira) && sira >= 1 && sira <= 4_000_000 && tur in PUAN_TURLERI
);
}
function aiHataMesaji(err: unknown): string {
if (err instanceof RaporUretimHatasi) return err.message;
if (err instanceof Error && err.message === "AI_KEY_MISSING") {
return "AI altyapısı henüz yapılandırılmadı (ANTHROPIC_API_KEY eksik).";
}
console.error("[liste] üretim hatası:", err);
return "Liste üretilirken bir sorun oluştu. Birazdan tekrar dener misin?";
}
function paramsBirlestir(
sira: number,
tur: PuanTuruKey,
secimler: SihirbazSecimleri,
): RaporParams {
return {
sira: Math.round(sira),
tur,
kategoriler: secimler.kategoriler,
iller: secimler.iller,
universiteTipi: secimler.universiteTipi,
oncelikler: secimler.oncelikler,
};
}
export async function listeOlustur(input: {
sira: number;
tur: string;
secimler: unknown;
requestId: string;
}): Promise<ListeSonuc> {
const session = await getSession();
if (!session) {
return { ok: false, code: "AUTH", error: "Giriş yapmalısın." };
}
const user = await getCurrentUser();
if (!user) {
return { ok: false, code: "AUTH", error: "Giriş yapmalısın." };
}
if (!user.hasPaket) {
return { ok: false, code: "PAKET", error: "Paket gerekli." };
}
if (!siraTurGecerli(input.sira, input.tur)) {
return { ok: false, code: "HATA", error: "Geçerli bir sıralama gerekli." };
}
const secimler = sihirbazDogrula(input.secimler);
if (!secimler) {
return {
ok: false,
code: "HATA",
error: "En az bir ilgi alanı seçmelisin.",
};
}
if (!input.requestId || typeof input.requestId !== "string") {
return { ok: false, code: "HATA", error: "Geçersiz istek." };
}
const params = paramsBirlestir(input.sira, input.tur, secimler);
// 3 kredi düş (idempotent: aynı requestId ile retry çifte harcamaz)
const harcama = await spendCredits({
userId: user.id,
amount: RAPOR_KREDI,
reason: "report_generate",
refId: input.requestId,
});
if (!harcama.ok) {
if (harcama.error === "INSUFFICIENT") {
return {
ok: false,
code: "KREDI",
error: `Liste oluşturmak için ${RAPOR_KREDI} kredi gerekli.`,
};
}
// DUPLICATE: bu requestId zaten işlendi → mevcut raporu döndür
const mevcut = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
if (mevcut?.result) {
return {
ok: true,
rapor: mevcut.result as RaporSonuc,
params: mevcut.params as RaporParams,
revisionCount: mevcut.revisionCount,
kredi: user.creditBalance,
};
}
return { ok: false, code: "HATA", error: "İstek tekrarlandı." };
}
let sonuc: RaporSonuc;
try {
sonuc = await raporUret(params);
} catch (err) {
// Üretim başarısız → krediyi iade et
await grantCredits({
userId: user.id,
delta: RAPOR_KREDI,
reason: "refund",
refId: input.requestId,
}).catch(() => {});
return { ok: false, code: "HATA", error: aiHataMesaji(err) };
}
const now = new Date();
await appDb
.insert(schema.reports)
.values({
id: crypto.randomUUID(),
userId: user.id,
params,
result: sonuc,
revisionCount: 0,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.reports.userId,
set: { params, result: sonuc, revisionCount: 0, updatedAt: now },
});
return {
ok: true,
rapor: sonuc,
params,
revisionCount: 0,
kredi: user.creditBalance - RAPOR_KREDI,
};
}
export async function listeRevize(input: {
feedback: string;
requestId: string;
}): Promise<ListeSonuc> {
const session = await getSession();
if (!session) return { ok: false, code: "AUTH", error: "Giriş yapmalısın." };
const user = await getCurrentUser();
if (!user) return { ok: false, code: "AUTH", error: "Giriş yapmalısın." };
if (!user.hasPaket) return { ok: false, code: "PAKET", error: "Paket gerekli." };
const feedback = (input.feedback ?? "").trim();
if (feedback.length < 5) {
return {
ok: false,
code: "HATA",
error: "Ne değişsin istediğini kısaca yazmalısın.",
};
}
if (!input.requestId) {
return { ok: false, code: "HATA", error: "Geçersiz istek." };
}
const rapor = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
if (!rapor?.result) {
return { ok: false, code: "HATA", error: "Önce bir liste oluşturmalısın." };
}
if (rapor.revisionCount >= MAX_REVIZYON) {
return {
ok: false,
code: "HATA",
error: `Revizyon hakkın doldu (en fazla ${MAX_REVIZYON}).`,
};
}
const params = rapor.params as RaporParams;
const onceki = rapor.result as RaporSonuc;
const harcama = await spendCredits({
userId: user.id,
amount: RAPOR_KREDI,
reason: "report_revision",
refId: input.requestId,
});
if (!harcama.ok) {
if (harcama.error === "INSUFFICIENT") {
return {
ok: false,
code: "KREDI",
error: `Revizyon için ${RAPOR_KREDI} kredi gerekli.`,
};
}
return { ok: false, code: "HATA", error: "İstek tekrarlandı." };
}
let sonuc: RaporSonuc;
try {
sonuc = await raporUret(params, feedback.slice(0, 1000), listeOzetiCikar(onceki));
} catch (err) {
await grantCredits({
userId: user.id,
delta: RAPOR_KREDI,
reason: "refund",
refId: input.requestId,
}).catch(() => {});
return { ok: false, code: "HATA", error: aiHataMesaji(err) };
}
// Sayaç yalnızca başarılı üretimde artar
await appDb
.update(schema.reports)
.set({
result: sonuc,
revisionCount: sql`${schema.reports.revisionCount} + 1`,
updatedAt: new Date(),
})
.where(eq(schema.reports.id, rapor.id));
return {
ok: true,
rapor: sonuc,
params,
revisionCount: rapor.revisionCount + 1,
kredi: user.creditBalance - RAPOR_KREDI,
};
}