chore(payments): remove EFT/bank-transfer method, keep Stripe only
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

Removes the EFT (havale) payment surface across API, web, i18n, and docs.
Card payment via Stripe is now the only checkout path.

API (apps/api/src):
- Delete payments/bank-accounts/ module (controller + service + module)
- payments.service: drop EFT methods (createEftPayment, uploadEftReceipt,
  approveEft, rejectEft, getActiveBankInfo, getPendingEftPayments) and the
  BankAccountsService dependency
- payments.controller: drop /payments/eft, /payments/eft/:id/{receipt,approve,reject},
  /payments/bank-info, /payments/pending; keep /payments/me
- payments.module: drop BankAccountsModule import
- admin.service: drop pending EFT counter from getDashboardStats; drop
  getPendingPayments (admin EFT approval list)
- admin.controller: drop /admin/payments/pending endpoint
- admin.service.spec: drop the getPendingPayments + pendingPayments assertions

Web (apps/web/src):
- Delete components/payment/bank-transfer-card.tsx
- Delete routes/dashboard/admin/payments.tsx (EFT approval page); regenerate
  routeTree.gen.ts
- subscription/index.tsx: drop EFT tab from PaymentMethodSection, drop the
  paymentMethod state + handleEftCompleted, simplify ConfirmationCard to the
  Stripe-only path, drop unused imports (Tabs, BankTransferCard, Building2)
- billing.tsx: drop "eft" from method filter chips; keep methodLabels.eft and
  the legacy receipt download so historical EFT records still display
- dashboard.tsx + admin/index.tsx: drop /dashboard/admin/payments from the
  sidebar + admin quick-link grid; trim unused icon imports; KEYS_6 → KEYS_5
  for the now-5-card admin stats skeleton
- messages/{tr,en}.json: strip every EFT-only key under payment.* (eftTransfer,
  eftConfirmationDescription, eftPaid, eftStatus, bank.*, uploadReceipt* etc.)

Schema (apps/api/src/database/schema):
- bankAccounts table + payments.bankAccountId column are kept as-is: legacy
  EFT payments remain visible on the billing page and the schema preserves
  historical records.

Docs:
- README.md: "Stripe (kart) + EFT" → "Stripe (kart)"
- CLAUDE.md: stack table + PaymentsModule row + payments table note + route
  list updated; admin EFT approval route removed

Verification:
- pnpm typecheck: green (api + web + shared + config + ui)
- pnpm lint: green
- pnpm test: 20 web tests pass, 173 api tests pass
This commit is contained in:
Fusion
2026-05-15 17:24:06 +03:00
committed by Semih Yesilyurt
parent 1278ef679d
commit 65daf99b2e
20 changed files with 555 additions and 2135 deletions

View File

@@ -13,7 +13,6 @@ import {
BookOpen,
Copy,
CreditCard,
DollarSign,
FlaskConical,
History,
LayoutDashboard,
@@ -67,7 +66,6 @@ const supportItems = [
const adminItems = [
{ to: "/dashboard/admin", label: "Yönetim Paneli", icon: Shield },
{ to: "/dashboard/admin/users", label: "Kullanıcılar", icon: Users },
{ to: "/dashboard/admin/payments", label: "Ödemeler", icon: DollarSign },
{ to: "/dashboard/admin/analytics", label: "Analitik", icon: BarChart3 },
{ to: "/dashboard/admin/copy-logs", label: "OEM Kopyalama", icon: Copy },
{ to: "/dashboard/admin/referrals", label: "Referanslar", icon: Share2 },

View File

@@ -1,7 +1,7 @@
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { KEYS_6 } from "@/lib/keys";
import { KEYS_5 } from "@/lib/keys";
import { formatTRY } from "@sase/shared";
import { Card, CardContent } from "@sase/ui";
import { Skeleton } from "@sase/ui";
@@ -10,10 +10,8 @@ import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import {
Activity,
Clock,
Copy,
CreditCard,
Receipt,
Search,
TrendingUp,
UserCog,
@@ -39,7 +37,6 @@ interface DashboardStats {
totalRevenue: number;
totalQueries: number;
newUsersThisMonth: number;
pendingPayments: number;
}
interface DailyStat {
@@ -77,7 +74,7 @@ function AdminDashboardPage() {
<div className="space-y-6">
<Skeleton className="h-8 w-48" />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{KEYS_6.map((__k) => (
{KEYS_5.map((__k) => (
<Skeleton key={__k} className="h-32" />
))}
</div>
@@ -123,13 +120,6 @@ function AdminDashboardPage() {
color: "text-orange-600",
bg: "bg-orange-50",
},
{
label: "Bekleyen Odeme",
value: stats?.pendingPayments ?? 0,
icon: Clock,
color: "text-red-600",
bg: "bg-red-50",
},
];
const quickLinks = [
@@ -138,11 +128,6 @@ function AdminDashboardPage() {
label: "Kullanici Yonetimi",
icon: UserCog,
},
{
to: "/dashboard/admin/payments",
label: "Odeme Onaylari",
icon: Receipt,
},
{
to: "/dashboard/admin/analytics",
label: "Sorgu Analizi",
@@ -182,7 +167,7 @@ function AdminDashboardPage() {
{/* Stat Cards */}
{statsLoading ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{KEYS_6.map((__k) => (
{KEYS_5.map((__k) => (
<Skeleton key={__k} className="h-32" />
))}
</div>

View File

@@ -1,234 +0,0 @@
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { formatTRY } from "@sase/shared";
import { Card, CardContent } from "@sase/ui";
import { Button } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { AlertTriangle, CheckCircle, ExternalLink, Receipt, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { KEYS_5 } from "@/lib/keys";
export const Route = createFileRoute("/dashboard/admin/payments")({
component: AdminPaymentsPage,
});
interface PendingPayment {
id: string;
userId: string;
userName: string;
userEmail: string;
subscriptionId: string;
amount: number;
currency: string;
method: string;
status: string;
eftReceiptUrl: string | null;
createdAt: string;
}
function AdminPaymentsPage() {
const { t } = useTranslation();
const { user, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [confirmAction, setConfirmAction] = useState<{
id: string;
type: "approve" | "reject";
} | null>(null);
useEffect(() => {
if (!authLoading && user?.role !== "admin") {
navigate({ to: "/dashboard/search" });
}
}, [authLoading, user, navigate]);
const { data: payments, isLoading } = useQuery({
queryKey: ["admin", "payments", "pending"],
queryFn: () => api.get<PendingPayment[]>("/admin/payments/pending"),
enabled: user?.role === "admin",
});
const approveMutation = useMutation({
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/approve`, {}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
setConfirmAction(null);
},
});
const rejectMutation = useMutation({
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/reject`, {}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
setConfirmAction(null);
},
});
if (authLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-96 w-full" />
</div>
);
}
if (user?.role !== "admin") return null;
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("tr-TR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
return (
<div className="mx-auto max-w-5xl space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">EFT Odeme Onaylari</h2>
<Badge variant="secondary">{payments?.length ?? 0} bekleyen</Badge>
</div>
{isLoading ? (
<div className="space-y-4">
{KEYS_5.map((__k) => (
<Skeleton key={__k} className="h-32 w-full" />
))}
</div>
) : !payments || payments.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center gap-3 py-16 text-center">
<CheckCircle className="h-12 w-12 text-green-500" />
<p className="text-lg font-medium">Bekleyen odeme bulunmuyor</p>
<p className="text-sm text-muted-foreground">Tum EFT odemeleri islenmis durumda</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{payments.map((payment) => (
<Card key={payment.id}>
<CardContent className="p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
{/* User Info */}
<div className="space-y-1">
<p className="font-medium">{payment.userName}</p>
<p className="text-sm text-muted-foreground">{payment.userEmail}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{formatDate(payment.createdAt)}</span>
<span>|</span>
<span>ID: {payment.id.substring(0, 8)}...</span>
</div>
</div>
{/* Amount & Receipt */}
<div className="flex flex-col items-end gap-2">
<p className="text-xl font-bold">{formatTRY(payment.amount)}</p>
{payment.eftReceiptUrl ? (
<a
href={payment.eftReceiptUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
>
<ExternalLink className="h-3 w-3" />
Dekont Goruntule
</a>
) : (
<span className="inline-flex items-center gap-1 text-sm text-muted-foreground">
<Receipt className="h-3 w-3" />
Dekont yuklenmemis
</span>
)}
</div>
</div>
{/* Confirm Dialog */}
{confirmAction && confirmAction.id === payment.id ? (
<div className="mt-4 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 text-amber-600" />
<div className="flex-1">
<p className="font-medium">
{confirmAction.type === "approve"
? "Odemeyi onaylamak istediginize emin misiniz?"
: "Odemeyi reddetmek istediginize emin misiniz?"}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{confirmAction.type === "approve"
? "Bu islem aboneligi aktif hale getirecektir."
: "Bu islem odemeyi basarisiz olarak isaretleyecektir."}
</p>
<div className="mt-3 flex items-center gap-2">
<Button
size="sm"
variant={confirmAction.type === "approve" ? "default" : "destructive"}
disabled={approveMutation.isPending || rejectMutation.isPending}
onClick={() => {
if (confirmAction.type === "approve") {
approveMutation.mutate(payment.id);
} else {
rejectMutation.mutate(payment.id);
}
}}
>
{approveMutation.isPending || rejectMutation.isPending
? "Isleniyor..."
: "Evet, onayla"}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setConfirmAction(null)}
>
Iptal
</Button>
</div>
</div>
</div>
</div>
) : (
<div className="mt-4 flex items-center gap-2 border-t pt-4">
<Button
size="sm"
onClick={() =>
setConfirmAction({
id: payment.id,
type: "approve",
})
}
>
<CheckCircle className="mr-1 h-4 w-4" />
Onayla
</Button>
<Button
size="sm"
variant="destructive"
onClick={() =>
setConfirmAction({
id: payment.id,
type: "reject",
})
}
>
<XCircle className="mr-1 h-4 w-4" />
Reddet
</Button>
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}

View File

@@ -77,7 +77,7 @@ function BillingPage() {
{/* Method Filter */}
<div className="flex gap-1">
{["all", "stripe", "eft"].map((method) => (
{["all", "stripe"].map((method) => (
<Button
key={method}
variant={methodFilter === method ? "default" : "outline"}

View File

@@ -1,4 +1,3 @@
import { BankTransferCard } from "@/components/payment/bank-transfer-card";
import { StripeCheckoutButton } from "@/components/payment/stripe-checkout-button";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
@@ -25,17 +24,12 @@ import {
DialogTitle,
Separator,
Skeleton,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@sase/ui";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import {
AlertTriangle,
ArrowRight,
Building2,
Check,
CheckCircle2,
Clock,
@@ -189,7 +183,6 @@ export function SubscriptionPage() {
const [selectedPlanKey, setSelectedPlanKey] = useState<string | null>(null);
const [selectedBrandIds, setSelectedBrandIds] = useState<string[]>([]);
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">("monthly");
const [paymentMethod, setPaymentMethod] = useState<"stripe" | "eft">("stripe");
// ─── Page-level state ──────────────────────────────────────────────────────
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
@@ -343,7 +336,6 @@ export function SubscriptionPage() {
if (search.stripe === "success" && !stripeResultRef.current) {
stripeResultRef.current = true;
setStep("confirmation");
setPaymentMethod("stripe");
capture("stripe_redirect_returned", { result: "success", session_id: search.session_id });
} else if (search.stripe === "cancelled" && !stripeResultRef.current) {
stripeResultRef.current = true;
@@ -459,11 +451,6 @@ export function SubscriptionPage() {
}
}
function handleEftCompleted() {
setStep("confirmation");
queryClient.invalidateQueries({ queryKey: ["subscription"] });
}
function handleCancelClick() {
if (downgradePlan) {
const currentBrandIds = (subscription?.brands ?? []).map((b) => b.brandId);
@@ -675,9 +662,7 @@ export function SubscriptionPage() {
title={t("subscription.steps.payment.title")}
isActive={step === "payment"}
isComplete={step === "confirmation"}
summary={
paymentMethod === "stripe" ? t("payment.creditCard") : t("payment.eftTransfer")
}
summary={t("payment.creditCard")}
onEdit={() => setStep("payment")}
>
<PaymentMethodSection
@@ -685,18 +670,14 @@ export function SubscriptionPage() {
period={billingPeriod}
brandIds={selectedBrandIds}
totalAmount={totalAmount}
method={paymentMethod}
onMethodChange={setPaymentMethod}
onEftCompleted={handleEftCompleted}
/>
</StepCard>
)}
{step === "confirmation" && (
<ConfirmationCard
method={paymentMethod}
subscription={subscription ?? null}
isStripeVerifying={paymentMethod === "stripe" && subscription?.status !== "active"}
isStripeVerifying={subscription?.status !== "active"}
navigate={navigate}
/>
)}
@@ -1100,50 +1081,21 @@ function PaymentMethodSection({
period,
brandIds,
totalAmount,
method,
onMethodChange,
onEftCompleted,
}: {
planKey: string;
period: "monthly" | "yearly";
brandIds: string[];
totalAmount: number;
method: "stripe" | "eft";
onMethodChange: (m: "stripe" | "eft") => void;
onEftCompleted: () => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-4">
<Tabs value={method} onValueChange={(v) => onMethodChange(v as "stripe" | "eft")}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="stripe" data-faro-user-action-name="payment-tab-card">
<CreditCard className="mr-2 h-4 w-4" />
{t("payment.creditCard")}
</TabsTrigger>
<TabsTrigger value="eft" data-faro-user-action-name="payment-tab-eft">
<Building2 className="mr-2 h-4 w-4" />
{t("payment.eftTransfer")}
</TabsTrigger>
</TabsList>
<TabsContent value="stripe" className="pt-4">
<StripeCheckoutButton
planKey={planKey}
period={period}
brandIds={brandIds}
totalAmount={totalAmount}
/>
</TabsContent>
<TabsContent value="eft" className="pt-4">
<BankTransferCard
planKey={planKey}
period={period}
brandIds={brandIds}
totalAmount={totalAmount}
onCompleted={onEftCompleted}
/>
</TabsContent>
</Tabs>
<StripeCheckoutButton
planKey={planKey}
period={period}
brandIds={brandIds}
totalAmount={totalAmount}
/>
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-xs text-foreground/70">
<span aria-hidden="true">🔒</span>
@@ -1160,18 +1112,16 @@ function PaymentMethodSection({
}
function ConfirmationCard({
method,
subscription,
isStripeVerifying,
navigate,
}: {
method: "stripe" | "eft";
subscription: Subscription | null;
isStripeVerifying: boolean;
navigate: ReturnType<typeof useNavigate>;
}) {
const { t } = useTranslation();
if (method === "stripe" && isStripeVerifying) {
if (isStripeVerifying) {
return (
<Card className="border-primary/30 ring-1 ring-primary/10">
<CardContent className="flex flex-col items-center gap-4 py-12 text-center">
@@ -1192,13 +1142,9 @@ function ConfirmationCard({
<CheckCircle2 className="h-16 w-16 text-emerald-500" />
<div>
<h3 className="text-2xl font-bold">{t("payment.confirmation")}</h3>
<p className="mt-2 text-muted-foreground">
{method === "stripe"
? t("payment.confirmationDescription")
: t("payment.eftConfirmationDescription")}
</p>
<p className="mt-2 text-muted-foreground">{t("payment.confirmationDescription")}</p>
</div>
{subscription?.endDate && method === "stripe" && (
{subscription?.endDate && (
<div className="rounded-lg border border-border bg-background/60 px-4 py-2 text-sm">
<span className="text-muted-foreground">{t("subscription.endDate")}: </span>
<span className="font-medium">