Compare commits

...

2 Commits

Author SHA1 Message Date
bilalgursen
43b6ceca3e fix(listem): fark modalı paketliye kredi bedeli göstermez
Sunucu artık paketliden kredi düşmüyor; arayüz hâlâ "3 kredi düşer" ve
"Listemi güncelle — 3 kredi" diyordu. raporKredi prop'u paketliye 0
iner, modal tek `ucretsiz` bayrağıyla üç metin yerini buna bağlar.
Kredi mantığı (krediYeter, KrediYetersiz dalı) olduğu gibi kalır.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:07:01 +03:00
bilalgursen
4acfd27ab1 fix(kredi): paketliden liste ve revizyon kredisi düşmez
/paket "liste ve revizyon dâhil" diyor, kod her üretimde 3 kredi
düşüyordu. Tek `maliyet = user.hasPaket ? 0 : RAPOR_KREDI` değişkeni
harcama, iade ve dönen bakiye satırlarını birden kapatıyor.

spendCredits/grantCredits çağrıları bilerek duruyor: ledger'daki
UNIQUE(reason, ref_id) üretimin tek idempotency çapası ve DUPLICATE
dalındaki "önceki deneme başarısızdı" çıkarımı refund satırının
varlığına dayanıyor. İade tutarı da koşullu — yoksa harcaması 0 olan
paketli her başarısız denemede bedava 3 kredi kazanırdı.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:06:52 +03:00
3 changed files with 57 additions and 16 deletions

View File

@@ -164,7 +164,12 @@ export async function ListemIcerik({
kilitAcilisi={kilitAcilisi} kilitAcilisi={kilitAcilisi}
kilitliBaslangic={user.hasPaket ? undefined : ACIK_SATIR} kilitliBaslangic={user.hasPaket ? undefined : ACIK_SATIR}
kredi={user.creditBalance} kredi={user.creditBalance}
raporKredi={RAPOR_KREDI} raporKredi={
/* Liste ve revizyon pakete dâhil (bkz. rapor-actions.ts'teki
maliyet): paketliye bedel 0 iner ki arayüz sunucunun yaptığını
söylesin. */
user.hasPaket ? 0 : RAPOR_KREDI
}
sohbetGecmisi={sohbetGecmisi as Mesaj[]} sohbetGecmisi={sohbetGecmisi as Mesaj[]}
ozet={danismanOzeti} ozet={danismanOzeti}
ilkYorum={ilkYorum} ilkYorum={ilkYorum}

View File

@@ -6,10 +6,12 @@
// alanların değiştiğini gösterir ve listeyi yeni tercihlere göre yeniden // alanların değiştiğini gösterir ve listeyi yeni tercihlere göre yeniden
// kurmayı önerir. // kurmayı önerir.
// //
// Yeniden kurulum normal üretimle AYNI kapıdan geçer (listeOlustur): 3 kredi, // Yeniden kurulum normal üretimle AYNI kapıdan geçer (listeOlustur): aynı
// aynı requestId idempotency'si, aynı kredi-yetersiz/iade davranışı. Ayrı bir // bedel, aynı requestId idempotency'si, aynı kredi-yetersiz/iade davranışı.
// "güncelleme" ürünü ya da indirimli yol yok — kullanıcı yeni bir liste alıyor, // Ayrı bir "güncelleme" ürünü ya da indirimli yol yok — kullanıcı yeni bir
// bedeli de yeni liste bedeli. // liste alıyor, bedeli de yeni liste bedeli. Paketli kullanıcıda o bedel 0:
// liste ve revizyon pakete dâhil (raporKredi prop'u listem-icerik.tsx'te
// koşullanır), krediler yalnız danışman sorusuna gider.
// //
// Renk: modalın kabuğu marka primary'si (bg-primary/10 ikon hücresi), ana // Renk: modalın kabuğu marka primary'si (bg-primary/10 ikon hücresi), ana
// aksiyon ise ürünün CTA turuncusu — kilidi açan, kredi harcayan, ödemeye // aksiyon ise ürünün CTA turuncusu — kilidi açan, kredi harcayan, ödemeye
@@ -127,6 +129,10 @@ export function TercihDegistiModali({
} }
const krediYeter = kredi >= raporKredi; const krediYeter = kredi >= raporKredi;
// Paketliye liste ve revizyon dâhil olduğu için bedel 0 iner (raporKredi
// prop'u listem-icerik.tsx'te koşullanıyor). Tek türetilmiş bayrak: modalın
// üç kredi cümlesi buna bakar, kredi mantığı olduğu gibi kalır.
const ucretsiz = raporKredi === 0;
return ( return (
<Dialog <Dialog
@@ -184,8 +190,17 @@ export function TercihDegistiModali({
tablosunda zaten duruyor; açıklama yalnızca sonucu ve tablosunda zaten duruyor; açıklama yalnızca sonucu ve
bedeli söylüyor. */} bedeli söylüyor. */}
<DialogDescription> <DialogDescription>
Listen eski tercihlerinle kuruldu. Yeni liste bunun yerine {ucretsiz ? (
geçer ve {raporKredi} kredi düşer. <>
Listen eski tercihlerinle kuruldu. Yeni liste bunun yerine
geçer; güncelleme paketine dâhil, kredi düşmez.
</>
) : (
<>
Listen eski tercihlerinle kuruldu. Yeni liste bunun yerine
geçer ve {raporKredi} kredi düşer.
</>
)}
</DialogDescription> </DialogDescription>
</div> </div>
</div> </div>
@@ -245,7 +260,7 @@ export function TercihDegistiModali({
{durum.ad === "hata" ? ( {durum.ad === "hata" ? (
<p className="text-sm text-red-700">{durum.mesaj}</p> <p className="text-sm text-red-700">{durum.mesaj}</p>
) : null} ) : null}
{!krediYeter && durum.ad !== "hata" ? ( {!ucretsiz && !krediYeter && durum.ad !== "hata" ? (
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
Güncelleme {raporKredi} kredi; bakiyen {kredi} kredi. Güncelleme {raporKredi} kredi; bakiyen {kredi} kredi.
</p> </p>
@@ -272,7 +287,9 @@ export function TercihDegistiModali({
onClick={() => void guncelle()} onClick={() => void guncelle()}
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600" className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
> >
Listemi güncelle — {raporKredi} kredi {ucretsiz
? "Listemi güncelle"
: `Listemi güncelle — ${raporKredi} kredi`}
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Button> </Button>
) : ( ) : (

View File

@@ -149,10 +149,18 @@ async function listeOlusturKilitli(
params: RaporParams, params: RaporParams,
requestId: string, requestId: string,
): Promise<ListeSonuc> { ): Promise<ListeSonuc> {
// Paket vaadi (docs/urun/vizyon.md): liste ve revizyon pakete dâhil; krediler
// yalnız danışman sorusu için. Çağrı BİLEREK duruyor — ledger'daki
// UNIQUE(reason, ref_id) (appdb/schema.ts:133) üretimin tek idempotency
// çapası; paketliye delta 0 satır yazılır, aynı requestId ikinci kez
// DUPLICATE döner. hasPaket tek yerden okunur (zaten elimizdeki user
// nesnesi) ki harcama ile iade aynı değeri görsün.
const maliyet = user.hasPaket ? 0 : RAPOR_KREDI;
// 3 kredi düş (idempotent: aynı requestId ile retry çifte harcamaz) // 3 kredi düş (idempotent: aynı requestId ile retry çifte harcamaz)
const harcama = await spendCredits({ const harcama = await spendCredits({
userId: user.id, userId: user.id,
amount: RAPOR_KREDI, amount: maliyet,
reason: "report_generate", reason: "report_generate",
refId: requestId, refId: requestId,
}); });
@@ -208,10 +216,13 @@ async function listeOlusturKilitli(
try { try {
sonuc = await raporUret(params); sonuc = await raporUret(params);
} catch (err) { } catch (err) {
// Üretim başarısız → krediyi iade et // Üretim başarısız → krediyi iade et. Satır paketlide de yazılır (delta 0):
// yukarıdaki DUPLICATE dalı "önceki deneme başarısız mıydı" sorusunu tam
// olarak bu refund kaydının varlığıyla cevaplıyor. Tutar koşullu, yoksa
// harcaması 0 olan paketliye her başarısız denemede bedava kredi yazardık.
await grantCredits({ await grantCredits({
userId: user.id, userId: user.id,
delta: RAPOR_KREDI, delta: maliyet,
reason: "refund", reason: "refund",
refId: requestId, refId: requestId,
}).catch(() => {}); }).catch(() => {});
@@ -240,7 +251,7 @@ async function listeOlusturKilitli(
rapor: user.hasPaket ? sonuc : raporMaskele(sonuc), rapor: user.hasPaket ? sonuc : raporMaskele(sonuc),
params, params,
revisionCount: 0, revisionCount: 0,
kredi: user.creditBalance - RAPOR_KREDI, kredi: user.creditBalance - maliyet,
hasPaket: user.hasPaket, hasPaket: user.hasPaket,
}; };
} }
@@ -282,9 +293,15 @@ export async function listeRevize(input: {
const params = rapor.params as RaporParams; const params = rapor.params as RaporParams;
const onceki = rapor.result as RaporSonuc; const onceki = rapor.result as RaporSonuc;
// Revizyon pakete dâhil (bkz. listeOlusturKilitli'deki aynı not). Yukarıdaki
// hasPaket kapısı yüzünden burada maliyet her zaman 0; ifade yine de koşullu
// yazılıyor ki kapı ileride gevşerse paketsiz kullanıcıdan ücret düşmeye
// devam etsin. Revizyon hakkını MAX_REVIZYON sayacı sınırlıyor, kredi değil.
const maliyet = user.hasPaket ? 0 : RAPOR_KREDI;
const harcama = await spendCredits({ const harcama = await spendCredits({
userId: user.id, userId: user.id,
amount: RAPOR_KREDI, amount: maliyet,
reason: "report_revision", reason: "report_revision",
refId: input.requestId, refId: input.requestId,
}); });
@@ -336,9 +353,11 @@ export async function listeRevize(input: {
listeOzetiCikar(onceki, params.sira), listeOzetiCikar(onceki, params.sira),
); );
} catch (err) { } catch (err) {
// İade satırı burada da yazılır (paketlide delta 0) — DUPLICATE dalındaki
// "önceki deneme başarısızdı" çıkarımı buna dayanıyor.
await grantCredits({ await grantCredits({
userId: user.id, userId: user.id,
delta: RAPOR_KREDI, delta: maliyet,
reason: "refund", reason: "refund",
refId: input.requestId, refId: input.requestId,
}).catch(() => {}); }).catch(() => {});
@@ -360,7 +379,7 @@ export async function listeRevize(input: {
rapor: sonuc, rapor: sonuc,
params, params,
revisionCount: rapor.revisionCount + 1, revisionCount: rapor.revisionCount + 1,
kredi: user.creditBalance - RAPOR_KREDI, kredi: user.creditBalance - maliyet,
hasPaket: true, hasPaket: true,
}; };
} }