fix(odeme): önce kredi sonra paid damgası; çift pakette iade akışı; UNIQUE tespiti
All checks were successful
Deploy / deploy (push) Successful in 8m50s

- odemeyiSonuclandir krediyi yazıp sonra paid damgalar; araya kesinti girerse
  sipariş pending kalır ve yeniden denenir (eskiden: paid görünür, kredi yok).
  paid + deftersiz eski siparişler ilk ziyarette onarılır
- paket tek seferlik: grantCredits({ paketTekil }) zaten paketliye yazmaz
  (kontrol UPDATE'in WHERE'inde). Sipariş paid damgalanır, senkron log +
  destek@ e-postası, /odeme/sonuc 'Bu ödeme iade edilecek' kartı, /kosullar#iade
- drizzle DrizzleQueryError üst mesajında 'UNIQUE' yok → idempotent no-op yerine
  hata fırlıyordu (grant + iki spend yolu); cause zincirine bakılıyor
- callback geçici DB hatasında 500 yerine sonuç sayfasına yönlendirir
- krediKaydiVarMi (reason, ref_id) indeksini kullanır; haftalık mutabakat sorgusu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 16:36:24 +03:00
parent 8f4669e31d
commit 024a2a49c4
6 changed files with 240 additions and 26 deletions

View File

@@ -1,9 +1,9 @@
import Link from "next/link";
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
import { ArrowRight, CheckCircle2, RotateCcw, XCircle } from "lucide-react";
import { verifySession } from "@/lib/session";
import { siparisGetir } from "../odeme-queries";
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
import { odemeyiSonuclandir } from "@/lib/odeme";
import { iadeEdilecekMi, odemeyiSonuclandir } from "@/lib/odeme";
import { Button } from "@/components/ui/button";
import { PagePixelDivider } from "@/components/pixel-decor";
import { Skeleton } from "@/components/ui/skeleton";
@@ -23,16 +23,54 @@ export async function OdemeSonucKarti({ siparis }: { siparis?: string }) {
// Yalnızca kendi siparişi görüntülenebilir
const sahibiMi = order?.userId === session.user.id;
// Self-healing: callback kaybolduysa burada tekrar doğrula
// Self-healing: callback kaybolduysa burada tekrar doğrula. "paid" siparişte
// de çağrılır — kredisi yazılmamış eski kayıtları onarır (idempotent).
const durum =
order && sahibiMi
? order.status === "paid"
? "paid"
: await odemeyiSonuclandir(order.id)
: "not_found";
order && sahibiMi ? await odemeyiSonuclandir(order.id) : "not_found";
const basarili = durum === "paid";
// Zaten paketliyken alınan ikinci paket ödemesi: kredi yazılmadı, iade edilir
const iade =
basarili && order
? await iadeEdilecekMi({ ...order, status: "paid" })
: false;
if (iade) {
return (
<div className="rounded-2xl border border-amber-200 bg-white p-8 shadow-sm">
<RotateCcw className="mx-auto size-12 text-amber-500" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Bu ödeme iade edilecek
</h1>
<p className="mt-2 text-sm text-slate-600">
Tercih Dönemi Paketi&apos;n zaten aktifti; paket tek seferlik olduğu
için bu ikinci ödemeyi iade ediyoruz — senin bir şey yapmana gerek
yok. İade en geç 14 gün içinde yapılır; kartına yansıması bankana
göre birkaç gün daha sürebilir. Sorun olursa{" "}
<a
href="mailto:destek@kolaytercih.com"
className="font-medium text-primary underline-offset-2 hover:underline"
>
destek@kolaytercih.com
</a>
&apos;a yaz.
</p>
<PagePixelDivider seed={79} className="mx-auto mt-5" />
<div className="mt-6 flex flex-col gap-2">
<Button
asChild
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href="/listem">
Listeme dön
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
</div>
</div>
);
}
// 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;