refactor: odeme feature'ı + son süpürme — geçiş tamamlandı (Faz 10)
Some checks failed
Deploy / deploy (push) Failing after 3m46s

- paket/actions.ts → odeme-actions.ts; satin-al-form + rapor-yok-cta
  features/odeme/components/ altına; odeme-queries.ts (siparisGetir)
- Yeni async OdemeSonucKarti: verifySession + sahiplik kontrolü + self-healing
  odemeyiSonuclandir çağrısı akıştaki yerinde (artık Suspense altındaki feature
  bileşeninde, aynı render semantiği); başarı/hata kartları içeride
- Yeni async PaketSatinAl: paket + top-up kartları, getCurrentUser okuması
  içeride; statik hero/dipnot sayfada kaldı
- İki sayfa senkron searchParams.then(); paket hata uyarısı sayfada çözülmüş
  JSX (hata yokken hiçbir şey çizilmediği için fallback bilerek null);
  paket/loading.tsx + odeme/sonuc/loading.tsx silindi
- Süpürme doğrulandı: app/ altında route-dışı bileşen dosyası kalmadı; eski
  yol grep'leri sıfır; 7 *-queries server-only, 3 *-actions 'use server';
  async sayfa sıfır; /api/odeme/callback el değmedi
- Davranış değişikliği yok

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:46:23 +03:00
parent 22e2fde058
commit d2732828cc
10 changed files with 373 additions and 345 deletions

View File

@@ -1,19 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
// /odeme/sonuc iyzico doğrulaması bekler — ortalanmış sonuç kartı iskeleti.
export default function Loading() {
return (
<main
aria-live="polite"
className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16"
>
<div className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
<Skeleton className="mx-auto size-12 rounded-full" />
<Skeleton className="mx-auto mt-5 h-7 w-2/3" />
<Skeleton className="mx-auto mt-3 h-5 w-full" />
<Skeleton className="mx-auto mt-2 h-5 w-3/4" />
<Skeleton className="mx-auto mt-6 h-12 w-full rounded-lg" />
</div>
</main>
);
}

View File

@@ -1,126 +1,29 @@
import Link from "next/link";
import { Suspense } from "react";
import type { Metadata } from "next";
import { eq } from "drizzle-orm";
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
import { verifySession } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
import { odemeyiSonuclandir } from "@/lib/odeme";
import { Button } from "@/components/ui/button";
import { PagePixelDivider } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer";
import { RaporYokCta } from "./rapor-yok-cta";
import {
OdemeSonucKarti,
OdemeSonucKartiSkeleton,
} from "@/features/odeme/components/odeme-sonuc-karti";
export const metadata: Metadata = {
title: "Ödeme Sonucu",
robots: { index: false, follow: false },
};
export default async function OdemeSonucPage({
export default function OdemeSonucPage({
searchParams,
}: {
searchParams: Promise<{ siparis?: string }>;
}) {
const { siparis } = await searchParams;
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın (başarı/unblur deneyimi)
const geriDonus = siparis
? `/odeme/sonuc?siparis=${encodeURIComponent(siparis)}`
: "/paket";
const session = await verifySession(geriDonus);
const order = siparis
? await appDb.query.orders.findFirst({
where: eq(schema.orders.id, siparis),
})
: null;
// Yalnızca kendi siparişi görüntülenebilir
const sahibiMi = order?.userId === session.user.id;
// Self-healing: callback kaybolduysa burada tekrar doğrula
const durum =
order && sahibiMi
? order.status === "paid"
? "paid"
: await odemeyiSonuclandir(order.id)
: "not_found";
const basarili = durum === "paid";
// Rapor zaten üretildiyse ödül bir tık uzakta: kullanıcıyı doğrudan
// rapor sayfasına döndür ki blur çözülme animasyonuyla tam listeyi görsün.
const rapor = basarili ? await kullanicininRaporu(session.user.id) : null;
const listeHref = rapor ? "/listem?acildi=1" : null;
return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
<main className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16 text-center">
{basarili ? (
<div className="rounded-2xl border border-emerald-200 bg-white p-8 shadow-sm">
<CheckCircle2
className="mx-auto size-12 text-emerald-500"
aria-hidden
/>
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme alındı 🎉
</h1>
<p className="mt-2 text-sm text-slate-600">
<span className="font-semibold">
{order!.credits} kredi
</span>{" "}
hesabına tanımlandı
{order!.product === "paket"
? " ve Tercih Dönemi Paketi'n aktif — kişisel listen, risk raporun ve PDF çıktın hazır olduğunda burada."
: "."}
</p>
<PagePixelDivider seed={79} className="mx-auto mt-5" />
<div className="mt-6 flex flex-col gap-2">
{listeHref ? (
<>
<Button
asChild
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={listeHref}>
Listeni aç — kilidi kaldırdık
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="text-center text-xs text-slate-500">
24 tercihin tamamı, gerekçeler, riskler ve PDF artık açık.
</p>
</>
) : (
<RaporYokCta />
)}
</div>
</div>
) : (
<div className="rounded-2xl border border-red-200 bg-white p-8 shadow-sm">
<XCircle className="mx-auto size-12 text-red-500" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme tamamlanamadı
</h1>
<p className="mt-2 text-sm text-slate-600">
{durum === "pending" ? (
"Ödemen hâlâ işleniyor. Birkaç saniye sonra bu sayfayı yenile — kartından çekim yapıldıysa krediler mutlaka tanımlanır."
) : (
<>
Çekim yapılmadı ya da işlem iptal edildi. Kartından para
çekildiğini düşünüyorsan{" "}
<Link href="/iletisim" className="underline">
bizimle iletişime geç
</Link>
.
</>
)}
</p>
<PagePixelDivider seed={83} className="mx-auto mt-5" />
<Button asChild variant="outline" className="mt-6 h-11 cursor-pointer">
<Link href="/paket">Tekrar dene</Link>
</Button>
</div>
)}
<Suspense fallback={<OdemeSonucKartiSkeleton />}>
{searchParams.then(({ siparis }) => (
<OdemeSonucKarti siparis={siparis} />
))}
</Suspense>
</main>
<SiteFooter />
</div>

View File

@@ -1,36 +0,0 @@
"use client";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
import { sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
/**
* Ödeme başarılı ama henüz Yapay Zeka listesi yokken gösterilen CTA: kayıtlı tercih
* profili olan adayı sihirbazı açık hâlde sonuç sayfasına götürür ve
* "sıralamanı gir" metni gösterilmez; profil yoksa ana sayfadaki sıralama
* formuna yönlendirir.
*/
export function RaporYokCta() {
const profil = useTercihProfili();
return (
<>
<Button
asChild
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={profil ? sonucHref(profil, { sihirbaz: "1" }) : "/"}>
24&apos;lük listeni oluştur
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="text-center text-xs text-slate-500">
{profil
? "Sıralaman kayıtlı — sonuç sayfasındaki sihirbazla listeni kur."
: "Sıralamanı girip sonuç sayfasındaki sihirbazla listeni kur."}
</p>
</>
);
}