Unify commercial runtime ownership across backend and frontend so platform, experience, and hospitality modules use the shared commercial source of truth. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
7.5 KiB
Python
159 lines
7.5 KiB
Python
# Generate payment feature pages + App Router routes
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(r"F:/TorbatYar/frontend")
|
||
MOD = ROOT / "modules" / "payment" / "features"
|
||
APP = ROOT / "app" / "payment"
|
||
|
||
# Feature implementations - compact but real API wired
|
||
FEATURES = {}
|
||
|
||
FEATURES["hub"] = r'''"use client";
|
||
|
||
import Link from "next/link";
|
||
import { CreditCard, LayoutDashboard, Wallet, Plug, Building2, Activity } from "lucide-react";
|
||
import { usePaymentCapabilities, usePaymentHealth } from "@/modules/payment/hooks/usePaymentCapabilities";
|
||
import { PaymentPageError, PaymentPageLoader } from "@/modules/payment/pages/shared";
|
||
|
||
const LINKS = [
|
||
{ href: "/payment/dashboard", label: "داشبورد اجرایی", icon: LayoutDashboard },
|
||
{ href: "/payment/requests", label: "درخواستهای پرداخت", icon: Wallet },
|
||
{ href: "/payment/psp", label: "مدیریت درگاه", icon: Plug },
|
||
{ href: "/payment/merchants", label: "پذیرندگان", icon: Building2 },
|
||
{ href: "/payment/monitoring", label: "مانیتورینگ", icon: Activity },
|
||
];
|
||
|
||
export function PaymentHub() {
|
||
const health = usePaymentHealth();
|
||
const caps = usePaymentCapabilities();
|
||
if (health.isLoading) return <PaymentPageLoader />;
|
||
if (health.isError) return <PaymentPageError error={health.error} onRetry={() => health.refetch()} />;
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-6">
|
||
<div className="flex items-center gap-3">
|
||
<CreditCard className="h-8 w-8 text-teal-600" />
|
||
<div>
|
||
<h1 className="text-2xl font-semibold text-secondary">تربتپی</h1>
|
||
<p className="text-sm text-[var(--muted)]">پلتفرم پرداخت سازمانی — MVP تا فاز ۱۴.۵</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 flex flex-wrap gap-3 text-xs text-[var(--muted)]">
|
||
<span>وضعیت سرویس: {health.data?.status}</span>
|
||
<span>نسخه: {health.data?.version}</span>
|
||
<span>فاز: {caps.data?.phase ?? "—"}</span>
|
||
<span>باندلها: {(caps.data?.active_bundles ?? []).join(", ") || "هیچ"}</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{LINKS.map((l) => {
|
||
const Icon = l.icon;
|
||
return (
|
||
<Link key={l.href} href={l.href} className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-4 hover:border-teal-600/50">
|
||
<Icon className="mb-2 h-5 w-5 text-teal-600" />
|
||
<p className="font-medium text-secondary">{l.label}</p>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
'''
|
||
|
||
FEATURES["dashboard"] = r'''"use client";
|
||
|
||
import { useMemo } from "react";
|
||
import { useQueries } from "@tanstack/react-query";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { paymentApi } from "@/modules/payment/services/payment-api";
|
||
import { PaymentBarChart, PaymentDonutChart, PaymentStatCards } from "@/modules/payment/components/PaymentCharts";
|
||
import { PaymentPageError, PaymentPageLoader } from "@/modules/payment/pages/shared";
|
||
import { usePaymentCapabilities } from "@/modules/payment/hooks/usePaymentCapabilities";
|
||
|
||
export function PaymentDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const caps = usePaymentCapabilities();
|
||
const [tx, req, audit] = useQueries({
|
||
queries: [
|
||
{ queryKey: ["payment", tenantId, "tx"], queryFn: () => paymentApi.transactions.list(tenantId!), enabled: !!tenantId },
|
||
{ queryKey: ["payment", tenantId, "req"], queryFn: () => paymentApi.requests.list(tenantId!), enabled: !!tenantId },
|
||
{ queryKey: ["payment", tenantId, "audit"], queryFn: () => paymentApi.audit.list(tenantId!), enabled: !!tenantId },
|
||
],
|
||
});
|
||
const loading = !tenantId || tx.isLoading || req.isLoading;
|
||
const err = tx.error || req.error;
|
||
const stats = useMemo(() => {
|
||
const requests = req.data ?? [];
|
||
const txs = tx.data ?? [];
|
||
const paid = requests.filter((r) => r.status === "paid").length;
|
||
const failed = requests.filter((r) => r.status === "failed").length;
|
||
const volume = txs.reduce((s, t) => s + Number(t.amount_minor ?? 0), 0);
|
||
const statusDist = Object.entries(
|
||
requests.reduce<Record<string, number>>((acc, r) => {
|
||
const k = String(r.status ?? "unknown");
|
||
acc[k] = (acc[k] ?? 0) + 1;
|
||
return acc;
|
||
}, {})
|
||
).map(([name, value]) => ({ name, value }));
|
||
return { paid, failed, volume, totalReq: requests.length, totalTx: txs.length, statusDist, recent: (audit.data ?? []).slice(0, 8) };
|
||
}, [req.data, tx.data, audit.data]);
|
||
|
||
if (loading) return <PaymentPageLoader />;
|
||
if (err) return <PaymentPageError error={err} onRetry={() => { tx.refetch(); req.refetch(); }} />;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h1 className="text-xl font-semibold text-secondary">داشبورد اجرایی تربتپی</h1>
|
||
<p className="text-sm text-[var(--muted)]">دادهها از API واقعی تراکنش، درخواست و حسابرسی</p>
|
||
</div>
|
||
<PaymentStatCards
|
||
items={[
|
||
{ label: "درخواستها", value: stats.totalReq },
|
||
{ label: "پرداختشده", value: stats.paid },
|
||
{ label: "ناموفق", value: stats.failed },
|
||
{ label: "حجم (minor)", value: stats.volume.toLocaleString("fa-IR"), hint: `باندلها: ${(caps.data?.active_bundles ?? []).join(", ") || "—"}` },
|
||
]}
|
||
/>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-4">
|
||
<h2 className="mb-2 text-sm font-medium text-secondary">توزیع وضعیت درخواستها</h2>
|
||
<PaymentDonutChart data={stats.statusDist} />
|
||
</div>
|
||
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-4">
|
||
<h2 className="mb-2 text-sm font-medium text-secondary">حجم تراکنشها بر اساس ارز</h2>
|
||
<PaymentBarChart
|
||
data={Object.entries(
|
||
(tx.data ?? []).reduce<Record<string, number>>((acc, t) => {
|
||
const c = String(t.currency ?? "IRR");
|
||
acc[c] = (acc[c] ?? 0) + Number(t.amount_minor ?? 0);
|
||
return acc;
|
||
}, {})
|
||
).map(([name, value]) => ({ name, value }))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-4">
|
||
<h2 className="mb-3 text-sm font-medium text-secondary">فعالیت اخیر (حسابرسی)</h2>
|
||
<ul className="space-y-2 text-sm">
|
||
{stats.recent.length === 0 ? (
|
||
<li className="text-[var(--muted)]">فعالیتی ثبت نشده</li>
|
||
) : (
|
||
stats.recent.map((a, i) => (
|
||
<li key={String(a.id ?? i)} className="flex justify-between border-b border-[var(--border)] py-2">
|
||
<span>{String(a.action)} — {String(a.entity_type)}</span>
|
||
<span className="text-xs text-[var(--muted)]">{a.created_at ? new Date(String(a.created_at)).toLocaleString("fa-IR") : "—"}</span>
|
||
</li>
|
||
))
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
'''
|
||
|
||
# Write generator that creates list-based features and remaining pages
|
||
print("writing generator body...")
|