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

149
src/lib/credits.ts Normal file
View File

@@ -0,0 +1,149 @@
import { and, eq, gte, sql } from "drizzle-orm";
import { appDb, schema } from "./appdb";
const { user, creditLedger, chatMessages } = schema;
export const URUNLER = {
paket: { amountKurus: 29900, credits: 60, label: "Tercih Dönemi Paketi" },
topup: { amountKurus: 12900, credits: 30, label: "+30 Kredi" },
} as const;
export const DENEME_KREDISI = 5;
// Rapor üretimi/revizyonu başına düşen kredi
export const RAPOR_KREDI = 3;
export type SpendResult =
| { ok: true }
| { ok: false; error: "INSUFFICIENT" | "DUPLICATE" };
function newId() {
return crypto.randomUUID();
}
/**
* Sabit tutarda kredi düşer + ledger kaydı — tek transaction, iade edilebilir.
* Rapor üretimi ve revizyonu için (3 kredi). refId idempotency anahtarıdır:
* aynı requestId ile ikinci çağrı DUPLICATE döner (çifte harcama olmaz).
* Bakiye yetersizse hiçbir şey yazılmaz.
*/
export async function spendCredits(opts: {
userId: string;
amount: number;
reason: "report_generate" | "report_revision";
refId: string;
}): Promise<SpendResult> {
try {
await appDb.transaction(async (tx) => {
const res = await tx
.update(user)
.set({
creditBalance: sql`${user.creditBalance} - ${opts.amount}`,
updatedAt: new Date(),
})
.where(
and(eq(user.id, opts.userId), gte(user.creditBalance, opts.amount)),
);
if (res.rowsAffected === 0) throw new Error("INSUFFICIENT");
await tx.insert(creditLedger).values({
id: newId(),
userId: opts.userId,
delta: -opts.amount,
reason: opts.reason,
refId: opts.refId,
createdAt: new Date(),
});
});
return { ok: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg === "INSUFFICIENT") return { ok: false, error: "INSUFFICIENT" };
if (msg.includes("UNIQUE")) return { ok: false, error: "DUPLICATE" };
throw err;
}
}
/**
* 1 kredi düşer + ledger kaydı + kullanıcı mesajını persist eder — tek transaction.
* Bakiye yetersizse hiçbir şey yazılmaz. clientMessageId unique → retry'da çifte
* harcama yerine DUPLICATE döner.
*/
export async function spendCreditForMessage(opts: {
userId: string;
content: string;
clientMessageId: string;
}): Promise<SpendResult & { messageId?: string }> {
const messageId = newId();
try {
await appDb.transaction(async (tx) => {
const res = await tx
.update(user)
.set({
creditBalance: sql`${user.creditBalance} - 1`,
updatedAt: new Date(),
})
.where(and(eq(user.id, opts.userId), gte(user.creditBalance, 1)));
if (res.rowsAffected === 0) throw new Error("INSUFFICIENT");
await tx.insert(chatMessages).values({
id: messageId,
userId: opts.userId,
role: "user",
content: opts.content,
clientMessageId: opts.clientMessageId,
createdAt: new Date(),
});
await tx.insert(creditLedger).values({
id: newId(),
userId: opts.userId,
delta: -1,
reason: "chat_message",
refId: messageId,
createdAt: new Date(),
});
});
return { ok: true, messageId };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg === "INSUFFICIENT") return { ok: false, error: "INSUFFICIENT" };
if (msg.includes("UNIQUE")) return { ok: false, error: "DUPLICATE" };
throw err;
}
}
/**
* Kredi tanımlama (deneme, satın alma, top-up, iade). UNIQUE(reason, refId)
* sayesinde aynı referansla ikinci çağrı sessizce no-op olur → idempotent.
*/
export async function grantCredits(opts: {
userId: string;
delta: number;
reason: "trial_grant" | "purchase" | "topup" | "refund";
refId: string;
setHasPaket?: boolean;
}): Promise<{ granted: boolean }> {
try {
await appDb.transaction(async (tx) => {
await tx.insert(creditLedger).values({
id: newId(),
userId: opts.userId,
delta: opts.delta,
reason: opts.reason,
refId: opts.refId,
createdAt: new Date(),
});
await tx
.update(user)
.set({
creditBalance: sql`${user.creditBalance} + ${opts.delta}`,
...(opts.setHasPaket ? { hasPaket: true } : {}),
updatedAt: new Date(),
})
.where(eq(user.id, opts.userId));
});
return { granted: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes("UNIQUE")) return { granted: false };
throw err;
}
}