Stop auto-failing manual payments (TPago/bank/cash) after the pending TTL.
Exclude manual providers from booking cleanup, hold them via the 72h sweep instead, and let admins reopen failed payments to pending. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -162,6 +162,22 @@ export default function AdminPaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReopen = async (payment: PaymentWithDetails) => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await paymentsApi.reopen(payment.id, noteText);
|
||||
toast.success(locale === 'es' ? 'Pago cambiado a pendiente' : 'Payment changed to pending');
|
||||
setSelectedPayment(null);
|
||||
setNoteText('');
|
||||
setSendEmail(true);
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || (locale === 'es' ? 'No se pudo reabrir el pago' : 'Failed to reopen payment'));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefund = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to process this refund?')) return;
|
||||
|
||||
@@ -503,19 +519,32 @@ export default function AdminPaymentsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={() => handleApprove(selectedPayment)} isLoading={processing} className="flex-1 min-h-[44px]">
|
||||
<CheckCircleIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Aprobar' : 'Approve'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleReject(selectedPayment)} isLoading={processing}
|
||||
className="flex-1 border-red-300 text-red-600 hover:bg-red-50 min-h-[44px]">
|
||||
<XCircleIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Rechazar' : 'Reject'}
|
||||
</Button>
|
||||
</div>
|
||||
{selectedPayment.status === 'failed' ? (
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={() => handleApprove(selectedPayment)} isLoading={processing} className="flex-1 min-h-[44px]">
|
||||
<CheckCircleIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Confirmar pago' : 'Confirm payment'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleReopen(selectedPayment)} isLoading={processing} className="flex-1 min-h-[44px]">
|
||||
<ArrowPathIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Cambiar a pendiente' : 'Change to pending'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={() => handleApprove(selectedPayment)} isLoading={processing} className="flex-1 min-h-[44px]">
|
||||
<CheckCircleIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Aprobar' : 'Approve'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleReject(selectedPayment)} isLoading={processing}
|
||||
className="flex-1 border-red-300 text-red-600 hover:bg-red-50 min-h-[44px]">
|
||||
<XCircleIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Rechazar' : 'Reject'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPayment.status !== 'on_hold' && (
|
||||
{!['on_hold', 'failed'].includes(selectedPayment.status) && (
|
||||
<div className="pt-2 border-t">
|
||||
<Button variant="outline" onClick={() => handleSendReminder(selectedPayment)} isLoading={sendingReminder} className="w-full min-h-[44px]">
|
||||
<EnvelopeIcon className="w-5 h-5 mr-2" />
|
||||
@@ -980,7 +1009,7 @@ export default function AdminPaymentsPage() {
|
||||
<td className="px-4 py-3">{getStatusBadge(payment.status)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval' || payment.status === 'on_hold') && (
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval' || payment.status === 'on_hold' || payment.status === 'failed') && (
|
||||
<Button size="sm" onClick={() => setSelectedPayment(payment)} className="text-xs px-2 py-1">
|
||||
{locale === 'es' ? 'Revisar' : 'Review'}
|
||||
</Button>
|
||||
@@ -1042,7 +1071,7 @@ export default function AdminPaymentsPage() {
|
||||
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
|
||||
<p className="text-[10px] text-gray-400">{formatDate(payment.createdAt)}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval' || payment.status === 'on_hold') && (
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval' || payment.status === 'on_hold' || payment.status === 'failed') && (
|
||||
<Button size="sm" onClick={() => setSelectedPayment(payment)} className="text-xs px-2.5 py-1.5 min-h-[36px]">
|
||||
{locale === 'es' ? 'Revisar' : 'Review'}
|
||||
</Button>
|
||||
|
||||
@@ -51,4 +51,10 @@ export const paymentsApi = {
|
||||
fetchApi<{ payment: Payment; message: string }>(`/api/payments/${id}/reactivate`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
reopen: (id: string, adminNote?: string) =>
|
||||
fetchApi<{ payment: Payment; message: string }>(`/api/payments/${id}/reopen`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ adminNote }),
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user