diff --git a/backend/src/payment/zibal.service.ts b/backend/src/payment/zibal.service.ts index bb7fd06..208dadf 100644 --- a/backend/src/payment/zibal.service.ts +++ b/backend/src/payment/zibal.service.ts @@ -453,7 +453,7 @@ export class ZibalService implements IPaymentGateway { // ========================================== // 7. پلتفرم مالی، کیف پول و استرداد وجه (Refund & Wallets) - // Base URL: https://api.zibal.ir with Bearer Token + // Base URL: https://api.zibal.ir with Bearer Token or apiKey in payload // ========================================== private async getAccessToken(): Promise { @@ -476,13 +476,25 @@ export class ZibalService implements IPaymentGateway { body?: Record, ): Promise { const token = await this.getAccessToken(); + const merchant = await this.getMerchant(); + const apiKey = token || merchant; + const headers: Record = { 'Content-Type': 'application/json', }; - if (token) { - headers['Authorization'] = `Bearer ${token}`; + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; } + // Automatically inject apiKey into body if it's a POST request and not provided + const finalBody = + body && method === 'POST' + ? { + apiKey: body.apiKey || apiKey, + ...body, + } + : body; + const url = `https://api.zibal.ir${endpoint}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); @@ -492,7 +504,7 @@ export class ZibalService implements IPaymentGateway { const res = await fetch(url, { method, headers, - body: body ? JSON.stringify(body) : undefined, + body: finalBody ? JSON.stringify(finalBody) : undefined, signal: controller.signal, }); diff --git a/frontend/admin-panel/src/pages/Transactions.tsx b/frontend/admin-panel/src/pages/Transactions.tsx index 083acd7..69037aa 100644 --- a/frontend/admin-panel/src/pages/Transactions.tsx +++ b/frontend/admin-panel/src/pages/Transactions.tsx @@ -22,7 +22,9 @@ import { Activity, Globe, Monitor, - Code + Code, + Send, + Undo2, } from 'lucide-react'; import { toast } from 'react-hot-toast'; import api from '../services/api'; @@ -111,6 +113,59 @@ export default function Transactions() { const [liveInquiryData, setLiveInquiryData] = useState(null); const [showRawLogs, setShowRawLogs] = useState(false); + // Refund Modal State + const [refundModalOpen, setRefundModalOpen] = useState(false); + const [refundForm, setRefundForm] = useState({ + trackId: '', + amount: '', + tryReverse: true, + cardNumber: '', + description: '', + }); + const [isSubmittingRefund, setIsSubmittingRefund] = useState(false); + + const openRefundModal = (tx: Transaction) => { + setRefundForm({ + trackId: tx.trackId || '', + amount: String(tx.amount || ''), + tryReverse: true, + cardNumber: tx.cardNumber || '', + description: `استرداد وجه سفارش #${tx.order?.orderNumber || tx.orderId || tx.id}`, + }); + setRefundModalOpen(true); + }; + + const handleRefundSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!refundForm.trackId) { + toast.error('شناسه تراکنش (Track ID) الزامی است'); + return; + } + try { + setIsSubmittingRefund(true); + const res = await api.post('/payment/admin/refund', { + trackId: refundForm.trackId, + amount: refundForm.amount ? Number(refundForm.amount) * 10 : undefined, // Convert Toman to Rial + tryReverse: refundForm.tryReverse, + cardNumber: refundForm.cardNumber || undefined, + description: refundForm.description || undefined, + }); + + if (res.data?.result === 1 || res.data?.status === 1 || res.data?.success) { + toast.success(res.data?.message || 'درخواست استرداد وجه با موفقیت به زیبال ارسال شد.'); + setRefundModalOpen(false); + fetchTransactions(); + fetchStats(); + } else { + toast.error(res.data?.message || 'خطا در ثبت استرداد وجه در زیبال'); + } + } catch (e: any) { + toast.error(e?.response?.data?.message || 'خطا در ارتباط با سرور استرداد زیبال'); + } finally { + setIsSubmittingRefund(false); + } + }; + const fetchStats = async () => { try { const res = await api.get('/payment/admin/stats'); @@ -612,12 +667,22 @@ export default function Transactions() { setSelectedTx(tx); handleLiveInquiry(tx.id); }} - className="p-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-xl transition-all" + className="p-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-xl transition-all cursor-pointer" title="استعلام زنده از زیبال" > )} + + {tx.gateway === 'zibal' && tx.trackId && tx.status === 'VERIFIED' && ( + + )} @@ -848,6 +913,20 @@ export default function Transactions() { رد فیش کارت به کارت )} + {selectedTx.gateway === 'zibal' && selectedTx.trackId && selectedTx.status === 'VERIFIED' && ( + + )} + + +
+
+ + setRefundForm({ ...refundForm, trackId: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500 bg-gray-50 font-bold" + dir="ltr" + /> +
+ +
+ + setRefundForm({ ...refundForm, amount: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500" + dir="ltr" + /> +
+ +
+ + setRefundForm({ ...refundForm, cardNumber: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500" + dir="ltr" + /> +
+ +
+ +