Refactor monolithic modules and harden booking, email, and auth infrastructure.

Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-06-25 07:12:59 +00:00
co-authored by Cursor
parent f0e2de2834
commit 613bd7be1d
75 changed files with 7702 additions and 5580 deletions
@@ -0,0 +1,249 @@
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
CreditCardIcon,
BuildingLibraryIcon,
CheckCircleIcon,
ArrowTopRightOnSquareIcon,
} from '@heroicons/react/24/outline';
import { Event, PaymentOptionsConfig } from '@/lib/api';
import { formatPrice, getTpagoLink } from '@/lib/utils';
import type { BookingResult } from '../_types';
interface ManualPaymentStepProps {
bookingResult: BookingResult;
event: Event;
paymentConfig: PaymentOptionsConfig;
locale: string;
paidUnderDifferentName: boolean;
setPaidUnderDifferentName: (value: boolean) => void;
payerName: string;
setPayerName: (value: string) => void;
markingPaid: boolean;
onMarkPaymentSent: () => void;
}
export function ManualPaymentStep({
bookingResult,
event,
paymentConfig,
locale,
paidUnderDifferentName,
setPaidUnderDifferentName,
payerName,
setPayerName,
markingPaid,
onMarkPaymentSent,
}: ManualPaymentStepProps) {
const isBankTransfer = bookingResult.paymentMethod === 'bank_transfer';
const isTpago = bookingResult.paymentMethod === 'tpago';
const ticketCount = bookingResult.ticketCount || 1;
const totalAmount = (event?.price || 0) * ticketCount;
const tpagoLink = getTpagoLink(paymentConfig, ticketCount);
return (
<div className="section-padding">
<div className="container-page max-w-xl">
<Card className="p-6">
<div className="text-center mb-6">
<div className={`w-16 h-16 rounded-full ${isBankTransfer ? 'bg-green-100' : 'bg-blue-100'} flex items-center justify-center mx-auto mb-4`}>
{isBankTransfer ? (
<BuildingLibraryIcon className="w-8 h-8 text-green-600" />
) : (
<CreditCardIcon className="w-8 h-8 text-blue-600" />
)}
</div>
<h1 className="text-xl font-bold text-primary-dark mb-2">
{locale === 'es' ? 'Completa tu Pago' : 'Complete Your Payment'}
</h1>
<p className="text-gray-600">
{locale === 'es'
? 'Sigue las instrucciones para completar tu pago'
: 'Follow the instructions to complete your payment'}
</p>
</div>
{/* Amount to pay */}
<div className="bg-gray-50 rounded-lg p-4 mb-6 text-center">
<p className="text-sm text-gray-500 mb-1">
{locale === 'es' ? 'Monto a pagar' : 'Amount to pay'}
</p>
<p className="text-2xl font-bold text-primary-dark">
{event?.price !== undefined ? formatPrice(totalAmount, event.currency) : ''}
</p>
{ticketCount > 1 && (
<p className="text-sm text-gray-500 mt-1">
{ticketCount} tickets × {formatPrice(event?.price || 0, event?.currency || 'PYG')}
</p>
)}
</div>
{/* Bank Transfer Details */}
{isBankTransfer && (
<div className="space-y-4 mb-6">
<h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Datos Bancarios' : 'Bank Details'}
</h3>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 space-y-3">
{paymentConfig.bankName && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Banco' : 'Bank'}:</span>
<span className="font-medium">{paymentConfig.bankName}</span>
</div>
)}
{paymentConfig.bankAccountHolder && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Titular' : 'Account Holder'}:</span>
<span className="font-medium">{paymentConfig.bankAccountHolder}</span>
</div>
)}
{paymentConfig.bankAccountNumber && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Nro. Cuenta' : 'Account Number'}:</span>
<span className="font-medium font-mono">{paymentConfig.bankAccountNumber}</span>
</div>
)}
{paymentConfig.bankAlias && (
<div className="flex justify-between">
<span className="text-gray-600">Alias:</span>
<span className="font-medium">{paymentConfig.bankAlias}</span>
</div>
)}
{paymentConfig.bankPhone && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Teléfono' : 'Phone'}:</span>
<span className="font-medium">{paymentConfig.bankPhone}</span>
</div>
)}
</div>
{(locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes) && (
<p className="text-sm text-gray-600">
{locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes}
</p>
)}
</div>
)}
{/* TPago Link */}
{isTpago && (
<div className="space-y-4 mb-6">
<h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'}
</h3>
{tpagoLink && (
<a
href={tpagoLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium"
>
<ArrowTopRightOnSquareIcon className="w-5 h-5" />
{locale === 'es' ? 'Abrir TPago para Pagar' : 'Open TPago to Pay'}
</a>
)}
{(locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions) && (
<p className="text-sm text-gray-600">
{locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions}
</p>
)}
</div>
)}
{/* Reference */}
<div className="bg-gray-100 rounded-lg p-3 mb-6">
<p className="text-xs text-gray-500 mb-1">
{locale === 'es' ? 'Referencia de tu reserva' : 'Your booking reference'}
</p>
<p className="font-mono font-bold text-lg">{bookingResult.qrCode}</p>
</div>
{/* Manual verification notice */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div className="flex gap-3">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-blue-600 mt-0.5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
</div>
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">
{locale === 'es' ? 'Verificación manual' : 'Manual verification'}
</p>
<p className="text-blue-700">
{locale === 'es'
? 'El equipo de Spanglish revisará el pago manualmente. Tu reserva solo será confirmada después de recibir un email de confirmación de nuestra parte.'
: 'The Spanglish team will review the payment manually. Your booking is only confirmed after you receive a confirmation email from us.'}
</p>
</div>
</div>
</div>
{/* Paid under different name option */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={paidUnderDifferentName}
onChange={(e) => {
setPaidUnderDifferentName(e.target.checked);
if (!e.target.checked) setPayerName('');
}}
className="mt-1 w-4 h-4 text-primary-yellow border-gray-300 rounded focus:ring-primary-yellow"
/>
<div>
<span className="font-medium text-gray-700">
{locale === 'es'
? 'El pago está a nombre de otra persona'
: 'The payment is under another person\'s name'}
</span>
<p className="text-xs text-gray-500 mt-1">
{locale === 'es'
? 'Marcá esta opción si el pago fue realizado por un familiar o tercero.'
: 'Check this option if the payment was made by a family member or a third party.'}
</p>
</div>
</label>
{paidUnderDifferentName && (
<div className="mt-3 pl-7">
<Input
label={locale === 'es' ? 'Nombre del pagador' : 'Payer name'}
value={payerName}
onChange={(e) => setPayerName(e.target.value)}
placeholder={locale === 'es' ? 'Nombre completo del titular de la cuenta' : 'Full name of account holder'}
required
/>
</div>
)}
</div>
{/* Warning before I Have Paid button */}
<p className="text-sm text-center text-amber-700 font-medium mb-3">
{locale === 'es'
? 'Solo haz clic aquí después de haber completado el pago.'
: 'Only click this after you have actually completed the payment.'}
</p>
{/* I Have Paid Button */}
<Button
onClick={onMarkPaymentSent}
isLoading={markingPaid}
size="lg"
className="w-full"
disabled={paidUnderDifferentName && !payerName.trim()}
>
<CheckCircleIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Ya Realicé el Pago' : 'I Have Paid'}
</Button>
<p className="text-xs text-center text-gray-500 mt-4">
{locale === 'es'
? 'Tu reserva será confirmada una vez que verifiquemos el pago'
: 'Your booking will be confirmed once we verify the payment'}
</p>
</Card>
</div>
</div>
);
}