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>
This commit is contained in:
Michilis
2026-07-13 03:47:35 +00:00
co-authored by Cursor
parent 75e317d73e
commit 0d47156071
9 changed files with 55 additions and 50 deletions
@@ -8,11 +8,17 @@ import {
} from '@heroicons/react/24/outline';
import type { PaymentMethod, BookingResult } from '../_types';
export const rucPattern = /^\d{6,10}$/;
// Paraguayan RUC: 5-8 digit base + "-" + 1 check digit (DV), e.g. 1234567-9 or 80012345-0
export const rucPattern = /^\d{5,8}-\d$/;
/** Format RUC input: digits only, max 10. */
/** Sanitize RUC input: digits and a single user-typed dash, max 10 chars. No dash is auto-inserted. */
export function formatRuc(value: string): string {
return value.replace(/\D/g, '').slice(0, 10);
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. */
@@ -226,25 +226,10 @@ export function BookingFormStep({
onBlur={handleRucBlur}
placeholder={t('booking.form.rucPlaceholder')}
error={errors.ruc}
inputMode="numeric"
maxLength={10}
aria-label={t('booking.form.ruc')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('booking.form.preferredLanguage')}
</label>
<select
value={formData.preferredLanguage}
onChange={(e) => setFormData({ ...formData, preferredLanguage: e.target.value as 'en' | 'es' })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
>
<option value="en">English</option>
<option value="es">Español</option>
</select>
</div>
</div>
</Card>
@@ -12,7 +12,6 @@ export interface BookingFormData {
lastName: string;
email: string;
phone: string;
preferredLanguage: 'en' | 'es';
// Empty until the user explicitly picks a method (no default selection).
paymentMethod: PaymentMethod | '';
ruc: string;
@@ -5,7 +5,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext';
import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api';
import { formatDateLong, formatTime } from '@/lib/utils';
import { formatDateLong, formatTime, formatRucDisplay } from '@/lib/utils';
import { isSafeExternalUrl } from '@/lib/safeRedirect';
import toast from 'react-hot-toast';
import type {
@@ -56,7 +56,6 @@ export default function BookingPage() {
lastName: '',
email: '',
phone: '',
preferredLanguage: locale as 'en' | 'es',
paymentMethod: '',
ruc: '',
});
@@ -78,11 +77,10 @@ export default function BookingPage() {
}
};
// Validate RUC on blur (optional field: 610 digits)
// Validate RUC on blur (optional field: base + "-" + check digit)
const handleRucBlur = () => {
if (!formData.ruc) return;
const digits = formData.ruc.replace(/\D/g, '');
if (digits.length > 0 && !rucPattern.test(digits)) {
if (!rucPattern.test(formData.ruc)) {
setErrors({ ...errors, ruc: t('booking.form.errors.rucInvalidFormat') });
}
};
@@ -152,8 +150,7 @@ export default function BookingPage() {
lastName: prev.lastName || lastName,
email: prev.email || user.email || '',
phone: prev.phone || user.phone || '',
preferredLanguage: (user.languagePreference as 'en' | 'es') || prev.preferredLanguage,
ruc: prev.ruc || user.rucNumber || '',
ruc: prev.ruc || formatRucDisplay(user.rucNumber),
};
});
}
@@ -197,12 +194,9 @@ export default function BookingPage() {
newErrors.phone = t('booking.form.errors.phoneTooShort');
}
// RUC validation (optional field - 610 digits if filled)
if (formData.ruc.trim()) {
const digits = formData.ruc.replace(/\D/g, '');
if (!/^\d{6,10}$/.test(digits)) {
newErrors.ruc = t('booking.form.errors.rucInvalidFormat');
}
// RUC validation (optional field - base + "-" + check digit if filled)
if (formData.ruc.trim() && !rucPattern.test(formData.ruc.trim())) {
newErrors.ruc = t('booking.form.errors.rucInvalidFormat');
}
// Payment method must be explicitly chosen and currently enabled
@@ -301,9 +295,9 @@ export default function BookingPage() {
lastName: formData.lastName,
email: formData.email,
phone: formData.phone,
preferredLanguage: formData.preferredLanguage,
preferredLanguage: locale as 'en' | 'es',
paymentMethod: formData.paymentMethod as PaymentMethod,
...(formData.ruc.trim() && { ruc: formData.ruc.replace(/\D/g, '') }),
...(formData.ruc.trim() && { ruc: formData.ruc.trim() }),
// Include attendees array for multi-ticket bookings
...(allAttendees.length > 1 && { attendees: allAttendees }),
});