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:
Michilis
2026-07-20 03:53:51 +00:00
co-authored by Cursor
parent 0d47156071
commit 4772b85f3d
7 changed files with 267 additions and 53 deletions
+43 -14
View File
@@ -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>
+6
View File
@@ -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 }),
}),
};