Add payment module, BFF proxy, hub/dashboard/ops/PSP/merchant screens wired to real APIs, and mark payment-frontend complete in project status. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useParams } from "next/navigation";
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||
import { toast } from "sonner";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { paymentApi } from "@/modules/payment/services/payment-api";
|
||
import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
|
||
import { PageHeader, Card, CardContent, Button } from "@/components/ds";
|
||
import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
|
||
|
||
export const PaymentRequestDetail = function PaymentRequestDetailPage() {
|
||
const params = useParams();
|
||
const id = String(params?.id ?? "");
|
||
const { tenantId } = useTenantId();
|
||
const qc = useQueryClient();
|
||
|
||
const q = useQuery({
|
||
queryKey: ["payment", tenantId, "request", id],
|
||
queryFn: () => paymentApi.requests.get(tenantId!, id),
|
||
enabled: !!tenantId && !!id,
|
||
});
|
||
|
||
const cancelM = useMutation({
|
||
mutationFn: () => paymentApi.requests.cancel(tenantId!, id),
|
||
onSuccess: () => {
|
||
toast.success("درخواست لغو شد");
|
||
qc.invalidateQueries({ queryKey: ["payment", tenantId, "request", id] });
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const verifyM = useMutation({
|
||
mutationFn: () => paymentApi.requests.verify(tenantId!, id, { status: "paid" }),
|
||
onSuccess: () => {
|
||
toast.success("پرداخت تأیید شد");
|
||
qc.invalidateQueries({ queryKey: ["payment", tenantId, "request", id] });
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <PaymentPageLoader />;
|
||
if (q.error) return <PaymentPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
const r = q.data!;
|
||
const status = String(r.status);
|
||
const canCancel = ["draft", "pending", "redirect_issued"].includes(status);
|
||
const canVerify = ["redirect_issued", "pending"].includes(status);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title="جزئیات درخواست پرداخت"
|
||
description="Checkout session / PaymentIntent"
|
||
/>
|
||
<Card>
|
||
<CardContent className="space-y-3 p-4 text-sm">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-[var(--muted)]">وضعیت:</span>
|
||
<PaymentStatusChip status={status} />
|
||
</div>
|
||
<p><span className="text-[var(--muted)]">مبلغ:</span> {String(r.amount_minor)} {String(r.currency)}</p>
|
||
<p><span className="text-[var(--muted)]">منبع:</span> {String(r.payment_source)}</p>
|
||
<p><span className="text-[var(--muted)]">PSP:</span> {String(r.provider_code)}</p>
|
||
{r.redirect_url ? (
|
||
<p>
|
||
<span className="text-[var(--muted)]">redirect:</span>{" "}
|
||
<a href={String(r.redirect_url)} className="text-teal-600 hover:underline" target="_blank" rel="noreferrer">
|
||
{String(r.redirect_url)}
|
||
</a>
|
||
</p>
|
||
) : null}
|
||
<p><span className="text-[var(--muted)]">idempotency:</span> {String(r.idempotency_key ?? "—")}</p>
|
||
<div className="flex flex-wrap gap-2 pt-2">
|
||
{canCancel ? (
|
||
<Button size="sm" variant="outline" loading={cancelM.isPending} onClick={() => cancelM.mutate()}>
|
||
لغو
|
||
</Button>
|
||
) : null}
|
||
{canVerify ? (
|
||
<Button size="sm" loading={verifyM.isPending} onClick={() => verifyM.mutate()}>
|
||
تأیید پرداخت
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">داده کامل</h3>
|
||
<pre className="overflow-x-auto text-xs">{JSON.stringify(r, null, 2)}</pre>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PaymentRequestDetail;
|