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:
252
src/app/sonuc/actions.ts
Normal file
252
src/app/sonuc/actions.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user