import React, { useEffect, useState } from 'react'; import { Box, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Button, CircularProgress, Alert, Dialog, DialogTitle, DialogContent, DialogActions, TextField, MenuItem, Tabs, Tab } from '@mui/material'; import { apiClient } from '../api/client'; interface Dispute { id: string; amount: number; currency: string; status: string; reason: string | null; charge?: string | null; created: number; is_charge_refundable?: boolean; evidence?: { customer_email_address?: string | null; customer_name?: string | null; customer_purchase_ip?: string | null; access_activity_log?: string | null; uncategorized_text?: string | null; } | null; } interface DisputeListResponse { data: { object: string; url: string; has_more: boolean; data: Dispute[]; }; } interface DisputeResponse { data: Dispute; } interface FraudWarning { id: string; charge_id: string; payment_intent_id: string | null; user_id: string | null; amount: number; currency: string; status: string; action: string; action_at: string | null; reason_notes?: string | null; created_at: string | null; meta_info?: any; } interface FraudWarningListResponse { data: FraudWarning[]; } interface FraudWarningResponse { data: FraudWarning; } const StripeDisputesDashboard: React.FC = () => { const [disputes, setDisputes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedDispute, setSelectedDispute] = useState(null); const [closing, setClosing] = useState(false); const [evidenceEmail, setEvidenceEmail] = useState(''); const [evidenceName, setEvidenceName] = useState(''); const [evidenceIp, setEvidenceIp] = useState(''); const [evidenceLog, setEvidenceLog] = useState(''); const [evidenceNotes, setEvidenceNotes] = useState(''); const [fraudType, setFraudType] = useState(''); const [submittingEvidence, setSubmittingEvidence] = useState(false); const [tab, setTab] = useState<'disputes' | 'fraudWarnings'>('disputes'); const [fraudWarnings, setFraudWarnings] = useState([]); const [fraudLoading, setFraudLoading] = useState(false); const [selectedWarning, setSelectedWarning] = useState(null); const [refundProcessing, setRefundProcessing] = useState(false); const [ignoreProcessing, setIgnoreProcessing] = useState(false); const [actionNotes, setActionNotes] = useState(''); const loadDisputes = async () => { setLoading(true); setError(null); try { const response = await apiClient.get('/api/subscription/disputes', { params: { limit: 20 }, }); const list = response.data?.data; setDisputes(Array.isArray(list?.data) ? list.data : []); } catch (e: any) { const message = e?.message || 'Failed to load disputes'; setError(message); } finally { setLoading(false); } }; const loadDisputeDetails = async (id: string) => { setError(null); try { const response = await apiClient.get(`/api/subscription/disputes/${id}`); if (response.data?.data) { setSelectedDispute(response.data.data); } } catch (e: any) { const message = e?.message || 'Failed to load dispute details'; setError(message); } }; const handleViewDetails = (id: string) => { loadDisputeDetails(id); }; const handleCloseDispute = async () => { if (!selectedDispute) return; setClosing(true); setError(null); try { await apiClient.post(`/api/subscription/disputes/${selectedDispute.id}/close`); await loadDisputes(); setSelectedDispute(null); } catch (e: any) { const message = e?.message || 'Failed to close dispute'; setError(message); } finally { setClosing(false); } }; const handleSubmitEvidence = async () => { if (!selectedDispute) return; setSubmittingEvidence(true); setError(null); try { const evidence: any = {}; if (evidenceEmail.trim()) { evidence.customer_email_address = evidenceEmail.trim(); } if (evidenceName.trim()) { evidence.customer_name = evidenceName.trim(); } if (evidenceIp.trim()) { evidence.customer_purchase_ip = evidenceIp.trim(); } if (evidenceLog.trim()) { evidence.access_activity_log = evidenceLog.trim(); } if (evidenceNotes.trim() || fraudType) { const prefix = fraudType ? `[fraud_type=${fraudType}] ` : ''; evidence.uncategorized_text = prefix + evidenceNotes.trim(); } if (Object.keys(evidence).length === 0) { setError('Please provide at least one evidence field before submitting.'); setSubmittingEvidence(false); return; } await apiClient.post(`/api/subscription/disputes/${selectedDispute.id}`, { evidence }); await loadDisputeDetails(selectedDispute.id); } catch (e: any) { const message = e?.message || 'Failed to submit evidence'; setError(message); } finally { setSubmittingEvidence(false); } }; const loadFraudWarnings = async () => { setFraudLoading(true); setError(null); try { const response = await apiClient.get('/api/subscription/fraud-warnings', { params: { status: 'open', limit: 20 }, }); const list = response.data?.data; setFraudWarnings(Array.isArray(list) ? list : []); } catch (e: any) { const message = e?.message || 'Failed to load fraud warnings'; setError(message); } finally { setFraudLoading(false); } }; const loadFraudWarningDetails = async (id: string) => { setError(null); try { const response = await apiClient.get(`/api/subscription/fraud-warnings/${id}`); if (response.data?.data) { setSelectedWarning(response.data.data); } } catch (e: any) { const message = e?.message || 'Failed to load fraud warning details'; setError(message); } }; const handleViewWarning = (id: string) => { loadFraudWarningDetails(id); }; const handleRefundWarning = async () => { if (!selectedWarning) return; setRefundProcessing(true); setError(null); try { const body: any = {}; if (actionNotes.trim()) { body.notes = actionNotes.trim(); } await apiClient.post(`/api/subscription/fraud-warnings/${selectedWarning.id}/refund`, body); await loadFraudWarnings(); await loadFraudWarningDetails(selectedWarning.id); } catch (e: any) { const message = e?.message || 'Failed to refund charge'; setError(message); } finally { setRefundProcessing(false); } }; const handleIgnoreWarning = async () => { if (!selectedWarning) return; setIgnoreProcessing(true); setError(null); try { const body: any = {}; if (actionNotes.trim()) { body.notes = actionNotes.trim(); } await apiClient.post(`/api/subscription/fraud-warnings/${selectedWarning.id}/ignore`, body); await loadFraudWarnings(); await loadFraudWarningDetails(selectedWarning.id); } catch (e: any) { const message = e?.message || 'Failed to update fraud warning'; setError(message); } finally { setIgnoreProcessing(false); } }; useEffect(() => { loadDisputes(); }, []); useEffect(() => { if (selectedDispute && selectedDispute.evidence) { const ev = selectedDispute.evidence; setEvidenceEmail(ev.customer_email_address || ''); setEvidenceName(ev.customer_name || ''); setEvidenceIp(ev.customer_purchase_ip || ''); setEvidenceLog(ev.access_activity_log || ''); if (ev.uncategorized_text) { setEvidenceNotes(ev.uncategorized_text); } else { setEvidenceNotes(''); } } else { setEvidenceEmail(''); setEvidenceName(''); setEvidenceIp(''); setEvidenceLog(''); setEvidenceNotes(''); setFraudType(''); } }, [selectedDispute]); useEffect(() => { if (tab === 'fraudWarnings') { loadFraudWarnings(); } }, [tab]); useEffect(() => { if (selectedWarning && selectedWarning.reason_notes) { setActionNotes(selectedWarning.reason_notes); } else { setActionNotes(''); } }, [selectedWarning]); const formatAmount = (amount: number, currency: string) => { const value = amount / 100; const code = (currency || 'usd').toUpperCase(); return `${value.toFixed(2)} ${code}`; }; const formatDate = (unix: number) => { if (!unix) return '-'; return new Date(unix * 1000).toLocaleString(); }; const formatIsoDate = (iso: string | null) => { if (!iso) return '-'; return new Date(iso).toLocaleString(); }; return ( Stripe Disputes Internal dashboard for viewing and managing Stripe disputes. This view is intended for admins only. Use this page to review Stripe disputes and submit clear evidence. For background on fraud patterns, see{' '} Common types of online fraud ,{' '} Card testing , and{' '} Identifying potential fraud . {error && ( {error} )} setTab(value)} > {tab === 'disputes' ? ( ) : ( )} {tab === 'disputes' ? ( loading ? ( ) : ( ID Amount Status Reason Charge Created Actions {disputes.length === 0 && ( No disputes found. )} {disputes.map((d) => ( {d.id} {formatAmount(d.amount, d.currency)} {d.status} {d.reason || '-'} {d.charge || '-'} {formatDate(d.created)} ))}
) ) : fraudLoading ? ( ) : ( ID Charge Amount Status Action Created Actions {fraudWarnings.length === 0 && ( No fraud warnings found. )} {fraudWarnings.map((fw) => ( {fw.id} {fw.charge_id} {formatAmount(fw.amount, fw.currency)} {fw.status} {fw.action} {formatIsoDate(fw.created_at)} ))}
)} setSelectedDispute(null)} maxWidth="sm" fullWidth > Dispute Details {selectedDispute && ( ID {selectedDispute.id} Amount {formatAmount(selectedDispute.amount, selectedDispute.currency)} Status {selectedDispute.status} Reason {selectedDispute.reason || '-'} Charge {selectedDispute.charge || '-'} Created {formatDate(selectedDispute.created)} Fraud Type setFraudType(e.target.value)} helperText="Choose the main fraud pattern: card testing, stolen card, overpayment, alternative refund, or other." > Not specified Card testing Stolen card Overpayment fraud Alternative refund Other Customer Email setEvidenceEmail(e.target.value)} /> Customer Name setEvidenceName(e.target.value)} /> Customer IP setEvidenceIp(e.target.value)} /> Access Activity Log setEvidenceLog(e.target.value)} /> Fraud Indicators / Notes setEvidenceNotes(e.target.value)} placeholder="Describe what looks suspicious: many failed attempts, overpayment + refund request, mismatched details, etc." helperText="Summaries here should match patterns described in Stripe docs: card testing spikes, stolen card indicators, overpayment/alternative refund scams." /> )} setSelectedWarning(null)} maxWidth="sm" fullWidth > Fraud Warning Details {selectedWarning && ( ID {selectedWarning.id} Charge {selectedWarning.charge_id} Amount {formatAmount(selectedWarning.amount, selectedWarning.currency)} Status {selectedWarning.status} Action {selectedWarning.action} Created {formatIsoDate(selectedWarning.created_at)} Last Action At {formatIsoDate(selectedWarning.action_at)} Issuer Fraud Type {selectedWarning.meta_info?.early_fraud_warning?.fraud_type || '-'} Actionable {selectedWarning.meta_info?.early_fraud_warning?.actionable === true ? 'Yes' : selectedWarning.meta_info?.early_fraud_warning?.actionable === false ? 'No' : '-'} Action Notes setActionNotes(e.target.value)} /> )}
); }; export default StripeDisputesDashboard;