feat(revizyon): revizyon sayısı sınırı kaldırıldı — tek koşul paket + sohbet kredisi

MAX_REVIZYON silindi; liste-revizyon.ts'teki HAK kapısı ve RevizyonSonucu'nun
"HAK" kodu kalktı, ozetCikar kalanHak döndürmüyor (sohbet-meta tipi de).
reports.revision_count sayaç olarak kalır (göç yok). /listem: "Revize et"
düğmesi paketlide hep görünür, "kalan hak" ifadeleri kalktı; sohbet-client
prop'u 0/1 bayrak olarak besleniyor (entegrasyonda revizyonAcik olacak).
Pazarlama metni: "Sınırsız liste revizyonu (danışmanla sohbetten)" (ana sayfa
ve paket kartı). Paketsiz-kredili adayda paket şartı korunur.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 23:16:34 +03:00
parent 15f4c4f4e1
commit e3d79f582e
8 changed files with 27 additions and 47 deletions

View File

@@ -28,7 +28,7 @@ import {
SectionEyebrow,
} from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
import { URUNLER } from "@/lib/credits";
import {
PROGRAM_SAYISI_CUMLE,
PROGRAM_SAYISI_METNI,
@@ -244,7 +244,7 @@ const paketOzellikleri = [
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
},
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
{ icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` },
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
{
icon: MessageCircleQuestion,
text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`,

View File

@@ -34,7 +34,7 @@ const OZELLIKLER = [
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
},
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
{
icon: MessageCircleQuestion,
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,

View File

@@ -75,7 +75,6 @@ export function ListemGovde({
danismanProfili,
ozet,
ilkYorum,
kalanHak,
paketFiyati,
paketKredi,
}: {
@@ -99,7 +98,6 @@ export function ListemGovde({
ozet?: DanismanOzeti;
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
ilkYorum?: IlkTercihYorumu;
kalanHak: number;
paketFiyati: string;
paketKredi: number;
}) {
@@ -216,12 +214,12 @@ export function ListemGovde({
}
if (hasPaket) {
satirlar.push(
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar (kalan hak: ${kalanHak})`,
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar`,
);
}
satirlar.push("Bu sohbetteki son mesajların");
return satirlar;
}, [adaySira, params, hasPaket, acikSatir, kalanHak]);
}, [adaySira, params, hasPaket, acikSatir]);
// Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez).
// Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları
@@ -331,8 +329,7 @@ export function ListemGovde({
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
kutusunu odaklar, aday ne değişsin istediğini yazar;
danışman gerekirse soru formuyla netleştirip listeyi
yeniden kurar. Hak bitince düğme kalkar. */}
{kalanHak > 0 ? (
yeniden kurar. Sayı sınırı yok (7 Eki 2026). */}
<Button
type="button"
variant="outline"
@@ -342,7 +339,6 @@ export function ListemGovde({
<PencilLine className="size-4" aria-hidden />
Revize et
</Button>
) : null}
<Button
asChild
variant="outline"
@@ -447,7 +443,8 @@ export function ListemGovde({
bildikleri={danismaninBildikleri}
genis={sohbetGenis}
onGenisDegistir={sohbetGenisligiAyarla}
revizyonHakki={kalanHak}
// 0/1 bayrak; prop adı entegrasyonda revizyonAcik olur
revizyonHakki={hasPaket ? 1 : 0}
/>
</div>
) : (

View File

@@ -9,7 +9,7 @@ import {
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
import { profilGetir } from "@/lib/danisman-profil-db";
import { profilSatirlari } from "@/lib/danisman-profil";
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
import { URUNLER, RAPOR_KREDI } from "@/lib/credits";
import type {
MaskeliRapor,
RaporParams,
@@ -177,7 +177,6 @@ export async function ListemIcerik({
danismanProfili={profilSatirlari(danismanProfili)}
ozet={danismanOzeti}
ilkYorum={ilkYorum}
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
paketFiyati={PAKET_FIYATI}
paketKredi={URUNLER.paket.credits}
/>

View File

@@ -75,7 +75,6 @@ export const SENARYOLAR: Senaryo[] = [
ok: true,
toplam: 24,
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
kalanHak: 1,
ilkTercihler: [
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
@@ -86,8 +85,7 @@ export const SENARYOLAR: Senaryo[] = [
});
writer.text(
"Listeni yeniden kurdum: ilk 5 tercihin artık İstanbul'dan, kalan 19 tercih Ankara ve İzmir'le devam ediyor. Dağılım yine 5 hayal / 13 dengeli / 6 güvenli.\n\n" +
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa\n\n" +
"Kalan revizyon hakkın: 1.",
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa",
);
})
.transport({ delayMs: 20 }),

View File

@@ -4,7 +4,6 @@ import { appDb, schema } from "@/lib/appdb";
import {
grantCredits,
krediBittiIsaretle,
MAX_REVIZYON,
RAPOR_KREDI,
spendCredits,
} from "@/lib/credits";
@@ -21,15 +20,15 @@ import type { RaporParams, RaporSonuc, RevizyonKurali } from "./types/rapor";
* (lib/ai/araclar.ts) burayı çağırır; eski /listem revizyon kutusu ve
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
*
* Koruma kapıları eski aksiyonla aynı: paket şartı, MAX_REVIZYON sayacı,
* kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
* Koruma kapıları: paket şartı (sayı sınırı yok — 7 Eki 2026; `revision_count`
* yalnız sayaç/istatistik), kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
* kimliği) ile idempotent defter kaydı, başarısız üretimde iade. Bedel
* paketliye 0 (liste ve revizyon pakete dâhil); satır yine yazılır ki aynı
* mesaj ikinci kez revizyon tetiklemesin.
*/
export type RevizyonSonucu =
| { ok: true; ozet: Extract<RevizyonOzeti, { ok: true }>; rapor: RaporSonuc }
| { ok: false; neden: string; kod: "PAKET" | "HAK" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
| { ok: false; neden: string; kod: "PAKET" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
function hataMesaji(err: unknown): string {
if (err instanceof RaporUretimHatasi) return err.message;
@@ -37,19 +36,18 @@ function hataMesaji(err: unknown): string {
err instanceof Error &&
/abort|timeout|timed out/i.test(`${err.name} ${err.constructor.name} ${err.message}`)
) {
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; revizyon hakkın kullanılmadı.";
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; kredin/paketin etkilenmedi.";
}
console.error("[revizyon] üretim hatası:", err instanceof Error ? err.message : String(err));
return "Liste yeniden kurulurken bir sorun oluştu; revizyon hakkın kullanılmadı.";
return "Liste yeniden kurulurken bir sorun oluştu; kredin/paketin etkilenmedi.";
}
function ozetCikar(rapor: RaporSonuc, revisionCount: number): Extract<RevizyonOzeti, { ok: true }> {
function ozetCikar(rapor: RaporSonuc): Extract<RevizyonOzeti, { ok: true }> {
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
return {
ok: true,
toplam: rapor.tercihler.length,
dagilim: { hayal: say("hayal"), dengeli: say("dengeli"), garanti: say("garanti") },
kalanHak: Math.max(0, MAX_REVIZYON - revisionCount),
ilkTercihler: rapor.tercihler.slice(0, 5).map((t) => {
const p = rapor.programlar[t.programId];
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
@@ -79,14 +77,6 @@ export async function listeRevizeCekirdek(input: {
if (!rapor?.result) {
return { ok: false, kod: "RAPOR", neden: "Adayın kayıtlı listesi yok; önce liste oluşturulmalı." };
}
if (rapor.revisionCount >= MAX_REVIZYON) {
return {
ok: false,
kod: "HAK",
neden: `Revizyon hakkı doldu (en fazla ${MAX_REVIZYON}).`,
};
}
const kilit = await uretimKilidiAl(input.userId, input.requestId);
if (!kilit.ok) {
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
@@ -123,7 +113,7 @@ export async function listeRevizeCekirdek(input: {
const guncel = await kullanicininRaporu(input.userId);
if (guncel?.result) {
const sonuc = guncel.result as RaporSonuc;
return { ok: true, ozet: ozetCikar(sonuc, guncel.revisionCount), rapor: sonuc };
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
}
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
}
@@ -157,7 +147,7 @@ export async function listeRevizeCekirdek(input: {
})
.where(eq(schema.reports.id, rapor.id));
return { ok: true, ozet: ozetCikar(sonuc, rapor.revisionCount + 1), rapor: sonuc };
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
} finally {
await uretimKilidiBirak(input.userId, input.requestId);
}

View File

@@ -47,7 +47,6 @@ export type RevizyonOzeti =
ok: true;
toplam: number;
dagilim: { hayal: number; dengeli: number; garanti: number };
kalanHak: number;
ilkTercihler: string[];
}
| { ok: false; neden: string };

View File

@@ -14,9 +14,6 @@ export const DENEME_KREDISI = 5;
// Rapor üretimi/revizyonu başına düşen kredi
export const RAPOR_KREDI = 3;
/** Paketle gelen geri bildirimli liste revizyonu hakkı */
export const MAX_REVIZYON = 2;
/**
* UNIQUE ihlali mi? drizzle sürücü hatasını DrizzleQueryError'a sarar: üst
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız