Added robust serial validation with support for multiple formats: - Production format: WELLNUO-XXXX-XXXX (strict validation) - Demo serials: DEMO-00000 and DEMO-1234-5678 - Legacy format: 8+ alphanumeric characters with hyphens Frontend improvements (activate.tsx): - Real-time validation feedback with error messages - Visual error indicators (red border, error icon) - Proper normalization (uppercase, trimmed) - Better user experience with clear error messages Backend improvements (beneficiaries.js): - Enhanced serial validation on activation endpoint - Stores normalized serial in device_id field - Better logging for debugging - Consistent error responses with validation details Testing: - 52 frontend tests covering all validation scenarios - 40 backend tests ensuring consistency - Edge case handling (long serials, special chars, etc.) Code quality: - ESLint configuration for test files - All tests passing - Zero linting errors
147 lines
3.5 KiB
TypeScript
147 lines
3.5 KiB
TypeScript
/**
|
|
* Serial Number Validation Utilities
|
|
*
|
|
* WellNuo device serial numbers follow formats:
|
|
* - Production: WELLNUO-XXXX-XXXX (16 chars with hyphens)
|
|
* - Demo: DEMO-00000 or DEMO-1234-5678
|
|
* - Legacy: 8+ alphanumeric characters
|
|
*/
|
|
|
|
export interface SerialValidationResult {
|
|
isValid: boolean;
|
|
format: 'production' | 'demo' | 'legacy' | 'invalid';
|
|
normalized: string; // Uppercase, trimmed
|
|
error?: string;
|
|
}
|
|
|
|
// Demo serial numbers
|
|
const DEMO_SERIALS = new Set([
|
|
'DEMO-00000',
|
|
'DEMO-1234-5678',
|
|
]);
|
|
|
|
/**
|
|
* Validates a serial number and returns detailed result
|
|
*/
|
|
export function validateSerial(serial: string | null | undefined): SerialValidationResult {
|
|
// Handle empty input
|
|
if (!serial || typeof serial !== 'string') {
|
|
return {
|
|
isValid: false,
|
|
format: 'invalid',
|
|
normalized: '',
|
|
error: 'Serial number is required',
|
|
};
|
|
}
|
|
|
|
// Normalize: trim and uppercase
|
|
const normalized = serial.trim().toUpperCase();
|
|
|
|
if (normalized.length === 0) {
|
|
return {
|
|
isValid: false,
|
|
format: 'invalid',
|
|
normalized: '',
|
|
error: 'Serial number is required',
|
|
};
|
|
}
|
|
|
|
// Check for demo serial
|
|
if (DEMO_SERIALS.has(normalized)) {
|
|
return {
|
|
isValid: true,
|
|
format: 'demo',
|
|
normalized,
|
|
};
|
|
}
|
|
|
|
// Production format: WELLNUO-XXXX-XXXX
|
|
// Must be exactly 16 characters with hyphens at positions 7 and 12
|
|
if (/^WELLNUO-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(normalized)) {
|
|
return {
|
|
isValid: true,
|
|
format: 'production',
|
|
normalized,
|
|
};
|
|
}
|
|
|
|
// Legacy format: minimum 8 alphanumeric characters (no special characters except hyphens)
|
|
if (/^[A-Z0-9\-]{8,}$/.test(normalized)) {
|
|
return {
|
|
isValid: true,
|
|
format: 'legacy',
|
|
normalized,
|
|
};
|
|
}
|
|
|
|
// Invalid format
|
|
let error = 'Invalid serial number format';
|
|
|
|
if (normalized.length < 8) {
|
|
error = 'Serial number must be at least 8 characters';
|
|
} else if (!/^[A-Z0-9\-]+$/.test(normalized)) {
|
|
error = 'Serial number can only contain letters, numbers, and hyphens';
|
|
}
|
|
|
|
return {
|
|
isValid: false,
|
|
format: 'invalid',
|
|
normalized,
|
|
error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Quick validation - returns true/false only
|
|
*/
|
|
export function isValidSerial(serial: string | null | undefined): boolean {
|
|
return validateSerial(serial).isValid;
|
|
}
|
|
|
|
/**
|
|
* Check if serial is a demo serial
|
|
*/
|
|
export function isDemoSerial(serial: string | null | undefined): boolean {
|
|
if (!serial) return false;
|
|
const normalized = serial.trim().toUpperCase();
|
|
return DEMO_SERIALS.has(normalized);
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly error message for invalid serial
|
|
*/
|
|
export function getSerialErrorMessage(serial: string | null | undefined): string {
|
|
const result = validateSerial(serial);
|
|
|
|
if (result.isValid) {
|
|
return '';
|
|
}
|
|
|
|
if (result.error) {
|
|
return result.error;
|
|
}
|
|
|
|
return 'Please enter a valid serial number from your WellNuo Starter Kit.\n\nFor testing, use: DEMO-00000';
|
|
}
|
|
|
|
/**
|
|
* Format serial for display (add hyphens if missing for production format)
|
|
*/
|
|
export function formatSerialForDisplay(serial: string): string {
|
|
const normalized = serial.trim().toUpperCase();
|
|
|
|
// If it looks like a production serial without hyphens, add them
|
|
if (/^WELLNUO[A-Z0-9]{8}$/.test(normalized)) {
|
|
return `${normalized.slice(0, 7)}-${normalized.slice(7, 11)}-${normalized.slice(11)}`;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Get placeholder text based on format preference
|
|
*/
|
|
export function getSerialPlaceholder(preferProduction: boolean = true): string {
|
|
return preferProduction ? 'WELLNUO-XXXX-XXXX' : 'Enter serial number';
|
|
}
|