Files
Spanglish/frontend/src/app/(public)/book/[eventId]/_logic/booking.ts
T
MichilisandCursor 0d47156071 Normalize RUC formatting to base-checkdigit across booking and admin views.
Accept dashed or digits-only RUC input on the backend, store a canonical dashed form, and display it consistently in booking and admin UIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 03:47:35 +00:00

138 lines
4.6 KiB
TypeScript

import toast from 'react-hot-toast';
import { PaymentOptionsConfig } from '@/lib/api';
import {
CreditCardIcon,
BanknotesIcon,
BoltIcon,
BuildingLibraryIcon,
} from '@heroicons/react/24/outline';
import type { PaymentMethod, BookingResult } from '../_types';
// Paraguayan RUC: 5-8 digit base + "-" + 1 check digit (DV), e.g. 1234567-9 or 80012345-0
export const rucPattern = /^\d{5,8}-\d$/;
/** Sanitize RUC input: digits and a single user-typed dash, max 10 chars. No dash is auto-inserted. */
export function formatRuc(value: string): string {
const cleaned = value.replace(/[^\d-]/g, '');
const firstDash = cleaned.indexOf('-');
const oneDash = firstDash === -1
? cleaned
: cleaned.slice(0, firstDash + 1) + cleaned.slice(firstDash + 1).replace(/-/g, '');
return oneDash.slice(0, 10);
}
/** Truncate a long invoice string for display. */
export function truncateInvoice(invoice: string, chars: number = 20): string {
if (invoice.length <= chars * 2) return invoice;
return `${invoice.slice(0, chars)}...${invoice.slice(-chars)}`;
}
/** Copy a Lightning invoice to the clipboard with localized feedback. */
export function copyInvoiceToClipboard(invoice: string, locale: string): void {
navigator.clipboard.writeText(invoice).then(() => {
toast.success(locale === 'es' ? '¡Copiado!' : 'Copied!');
}).catch(() => {
toast.error(locale === 'es' ? 'Error al copiar' : 'Failed to copy');
});
}
export interface PaymentMethodOption {
id: PaymentMethod;
icon: typeof CreditCardIcon;
label: string;
description: string;
badge?: string;
}
/** Build the list of selectable payment methods from the event config. */
export function buildPaymentMethods(
paymentConfig: PaymentOptionsConfig | null,
locale: string
): PaymentMethodOption[] {
const paymentMethods: PaymentMethodOption[] = [];
if (paymentConfig?.lightningEnabled) {
paymentMethods.push({
id: 'lightning',
icon: BoltIcon,
label: 'Bitcoin Lightning',
description: locale === 'es' ? 'Pago instantáneo con Bitcoin' : 'Instant payment with Bitcoin',
badge: locale === 'es' ? 'Instantáneo' : 'Instant',
});
}
if (paymentConfig?.tpagoEnabled) {
paymentMethods.push({
id: 'tpago',
icon: CreditCardIcon,
label: locale === 'es' ? 'TPago / Tarjetas de Crédito' : 'TPago / Credit Cards',
description: locale === 'es' ? 'Pagá con tarjetas de crédito locales o internacionales' : 'Pay with local or international credit cards',
badge: locale === 'es' ? 'Manual' : 'Manual',
});
}
if (paymentConfig?.bankTransferEnabled) {
paymentMethods.push({
id: 'bank_transfer',
icon: BuildingLibraryIcon,
label: locale === 'es' ? 'Transferencia Bancaria Local' : 'Local Bank Transfer',
description: locale === 'es' ? 'Pago por transferencia bancaria en Paraguay' : 'Pay via Paraguayan bank transfer',
badge: locale === 'es' ? 'Manual' : 'Manual',
});
}
if (paymentConfig?.cashEnabled) {
paymentMethods.push({
id: 'cash',
icon: BanknotesIcon,
label: locale === 'es' ? 'Efectivo en el Evento' : 'Cash at Event',
description: locale === 'es' ? 'Paga cuando llegues al evento' : 'Pay when you arrive at the event',
badge: locale === 'es' ? 'Manual' : 'Manual',
});
}
return paymentMethods;
}
export interface SuccessContent {
title: string;
description: string;
iconColor: string;
iconTextColor: string;
}
/** Resolve the success-screen copy based on the payment method used. */
export function getSuccessContent(
bookingResult: BookingResult | null,
locale: string,
t: (key: string) => string
): SuccessContent {
if (bookingResult?.paymentMethod === 'cash') {
return {
title: locale === 'es' ? '¡Reserva Recibida!' : 'Reservation Received!',
description: locale === 'es'
? 'Tu lugar está reservado. El pago se realizará en el evento.'
: 'Your spot is reserved. Payment will be collected at the event.',
iconColor: 'bg-yellow-100',
iconTextColor: 'text-yellow-600',
};
}
if (bookingResult?.paymentMethod === 'lightning') {
// For Lightning, if we're on success step, payment was confirmed
return {
title: locale === 'es' ? '¡Pago Confirmado!' : 'Payment Confirmed!',
description: locale === 'es'
? '¡Tu reserva está confirmada! Te esperamos en el evento.'
: 'Your booking is confirmed! See you at the event.',
iconColor: 'bg-green-100',
iconTextColor: 'text-green-600',
};
}
return {
title: t('booking.success.title'),
description: t('booking.success.description'),
iconColor: 'bg-green-100',
iconTextColor: 'text-green-600',
};
}