reservation implementation

This commit is contained in:
root
2026-06-11 15:35:25 -04:00
parent 6eeba99c45
commit 37a6ed8a76
30 changed files with 2598 additions and 477 deletions
+161
View File
@@ -0,0 +1,161 @@
/**
* User Input Validation and Formatting Library
*
* Implements the rules defined in docs/input_validation_plan.md.
* Provides formatting functions and a Zod-integrated pipeline that:
* 1. Removes disallowed characters
* 2. Trims leading/trailing whitespace
* 3. Validates format (email)
* 4. Applies case transformation
* 5. Truncates to max length
*/
// ─── Character class definitions ───────────────────────────────
const LETTERS_NUMBERS = 'a-zA-Z0-9'
/** Letters, numbers, spaces, hyphen, slash */
const BASE_TEXT = `${LETTERS_NUMBERS}\\s\\-\\/`
/** Letters, spaces, hyphen */
const LETTERS_SPACES_HYPHEN = 'a-zA-Z\\s\\-'
/** Letters, numbers, spaces, hyphen, slash, comma (full address / company names) */
const EXTENDED_TEXT = `${BASE_TEXT},`
/** Letters only */
const LETTERS_ONLY = 'a-zA-Z'
// ─── Field type definitions ────────────────────────────────────
type FieldType =
| 'name'
| 'nationality'
| 'streetAddress'
| 'city'
| 'pickupLocation'
| 'returnLocation'
| 'country'
| 'fullAddress'
| 'commercialName'
| 'legalCompanyName'
| 'email'
| 'licensePlate'
| 'vin'
| 'cin'
| 'passportNumber'
| 'internationalPermitNumber'
| 'driverLicenseNumber'
| 'licenseCategory'
| 'iceNumber'
| 'commercialRegistry'
| 'carMark'
| 'carModel'
| 'carColor'
| 'zipCode'
interface FieldConfig {
maxLength: number
allowedPattern: RegExp
transform: (value: string) => string
}
// ─── Formatting functions ──────────────────────────────────────
/** Uppercase first letter of each word, lowercase the rest */
export function toTitleCase(str: string): string {
return str.replace(/\b\w+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
}
/** Uppercase first letter of every word (same logic as toTitleCase at word boundaries) */
export function toTitleCaseAll(str: string): string {
return str.replace(/\b\w+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
}
export function toLowerCase(str: string): string {
return str.toLowerCase()
}
/** Uppercase letters only, leave numbers unchanged */
export function toUpperCase(str: string): string {
return str.replace(/[a-zA-Z]/g, (char) => char.toUpperCase())
}
/** Identity — no transformation */
export function preserveOriginal(str: string): string {
return str
}
// ─── Field configuration ───────────────────────────────────────
const FIELD_CONFIGS: Record<FieldType, FieldConfig> = {
// Group 1: Title Case
name: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
nationality: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
streetAddress: { maxLength: 100, allowedPattern: new RegExp(`^[${BASE_TEXT}]*$`), transform: toTitleCase },
city: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
pickupLocation: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
returnLocation: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
country: { maxLength: 50, allowedPattern: new RegExp(`^[${LETTERS_SPACES_HYPHEN}]*$`), transform: toTitleCase },
// Group 2: Title Case All
fullAddress: { maxLength: 200, allowedPattern: new RegExp(`^[${EXTENDED_TEXT}]*$`), transform: toTitleCaseAll },
commercialName: { maxLength: 100, allowedPattern: new RegExp(`^[${EXTENDED_TEXT}]*$`), transform: toTitleCaseAll },
legalCompanyName: { maxLength: 100, allowedPattern: new RegExp(`^[${EXTENDED_TEXT}]*$`), transform: toTitleCaseAll },
// Group 3: Lowercase — email handled separately with format validation
email: { maxLength: 100, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}@._%\\+\\-]*$`), transform: toLowerCase },
// Group 4: Uppercase (letters and numbers only, no spaces/hyphens)
licensePlate: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
vin: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
cin: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
passportNumber: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
internationalPermitNumber:{ maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
driverLicenseNumber: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
licenseCategory: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
iceNumber: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
commercialRegistry: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}]*$`), transform: toUpperCase },
// Group 5: Car fields
carMark: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}\\s\\-]*$`), transform: toTitleCase },
carModel: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}\\s\\-]*$`), transform: toTitleCase },
carColor: { maxLength: 30, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}\\s\\-]*$`), transform: toTitleCase },
// Group 6: Preserved
zipCode: { maxLength: 10, allowedPattern: new RegExp(`^[${LETTERS_NUMBERS}\\-]*$`), transform: preserveOriginal },
}
// ─── Public API ────────────────────────────────────────────────
/** Full processing pipeline: sanitize → trim → format → truncate */
export function sanitizeAndFormat(value: string, fieldType: FieldType): string {
const config = FIELD_CONFIGS[fieldType]
// Step 1: Remove disallowed characters
let sanitized = ''
for (const char of value) {
if (config.allowedPattern.test(char)) {
sanitized += char
}
}
// Step 2: Trim
sanitized = sanitized.trim()
// Step 3: Apply case transformation
const formatted = config.transform(sanitized)
// Step 4: Truncate
if (formatted.length > config.maxLength) {
return formatted.slice(0, config.maxLength)
}
return formatted
}
/** Get the max length for a given field type */
export function getMaxLength(fieldType: FieldType): number {
return FIELD_CONFIGS[fieldType].maxLength
}