From e3a5256281565e5e93b0bd0416d1414a2f2c5437 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 23 Jul 2025 17:51:39 +0200 Subject: [PATCH] Add new frontend - fix 24 --- frontend/src/api/services/dataService.ts | 253 +++++------------------ frontend/src/pages/onboarding.tsx | 219 ++++++++++---------- services/data/app/api/sales.py | 2 +- 3 files changed, 161 insertions(+), 313 deletions(-) diff --git a/frontend/src/api/services/dataService.ts b/frontend/src/api/services/dataService.ts index c7ec220b..425de14f 100644 --- a/frontend/src/api/services/dataService.ts +++ b/frontend/src/api/services/dataService.ts @@ -19,28 +19,13 @@ export interface UploadResponse { upload_id?: string; } -// ✅ FIXED: Updated to match backend SalesValidationResult schema export interface DataValidation { - is_valid: boolean; // Changed from 'valid' to 'is_valid' - total_records: number; // Changed from 'recordCount' to 'total_records' - valid_records: number; // Added missing field - invalid_records: number; // Added missing field - errors: Array<{ // Changed from string[] to object array - row?: number; - field?: string; - message: string; - value?: any; - }>; - warnings: Array<{ // Changed from string[] to object array - row?: number; - field?: string; - message: string; - value?: any; - }>; - summary: { // Added missing summary field - [key: string]: any; - }; - duplicates?: number; // Made optional, may not always be present + valid: boolean; // ✅ Backend uses "valid", not "is_valid" + errors: string[]; // ✅ Backend returns string array, not objects + warnings: string[]; // ✅ Backend returns string array, not objects + suggestions: string[]; // ✅ Backend uses "suggestions", not "summary" + recordCount?: number; // ✅ Optional field for record count (if backend provides it) + duplicates?: number; // ✅ Optional field for duplicates } // Data types @@ -104,11 +89,10 @@ export class DataService { * ✅ COMPLETELY FIXED: Validate sales data before upload * Backend expects JSON data with SalesDataImport structure, not a file upload */ - async validateSalesData(file: File): Promise { + async validateSalesData(file: File, tenantId?: string): Promise { try { console.log('Reading file content...', file.name); - // ✅ FIXED: Proper file reading implementation const fileContent = await this.readFileAsText(file); if (!fileContent) { @@ -128,219 +112,92 @@ export class DataService { } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { dataFormat = 'excel'; } else { - // Default to CSV if unable to determine dataFormat = 'csv'; } console.log('Detected file format:', dataFormat); - // ✅ CRITICAL FIX: Use correct endpoint based on API Gateway routing + // ✅ FIXED: Use proper tenant ID when available const importData: SalesDataImportRequest = { - tenant_id: '', // Will be set by backend from auth context + tenant_id: tenantId || '00000000-0000-0000-0000-000000000000', data: fileContent, data_format: dataFormat, validate_only: true }; - console.log('Sending validation request to:', '/api/v1/data/sales/import/validate'); + console.log('Sending validation request with tenant_id:', importData.tenant_id); - // ✅ FIXED: Correct endpoint path - Gateway routes /api/v1/data/sales/* to data service /api/v1/sales/* const response = await apiClient.post>( - '/api/v1/data/sales/import/validate', // Gateway will proxy this to data service + '/api/v1/data/sales/import/validate', importData ); console.log('Raw response from API:', response); - // ✅ CRITICAL FIX: Handle various response formats properly - // Check if response contains error information first + // ✅ FIXED: Handle response according to backend's actual format if (response && typeof response === 'object') { - // Handle direct error responses (like 404) + // Handle validation errors from FastAPI if ('detail' in response) { console.error('API returned error:', response.detail); - if (response.detail === 'Not Found') { - // Return a proper validation failure for missing endpoint - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: 'El servicio de validación no está disponible temporalmente. Por favor contacta al soporte técnico.' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'service_unavailable', - message: 'Validation service endpoint not found' + if (Array.isArray(response.detail)) { + // Handle Pydantic validation errors + const errorMessages = response.detail.map(err => { + if (typeof err === 'object' && err.msg) { + return `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`; } - }; - } else { - // Handle other API errors + return err.toString(); + }); + return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: `Error del servidor: ${response.detail}` - }], + valid: false, + errors: errorMessages, warnings: [], - summary: { - validation_error: true, - error_type: 'api_error', - message: response.detail - } + suggestions: ['Revisa el formato de los datos enviados'] }; } + + return { + valid: false, + errors: [typeof response.detail === 'string' ? response.detail : 'Error de validación'], + warnings: [], + suggestions: [] + }; } - // Handle successful response (either wrapped or direct) - const validationResult = response.data || response; - - // Verify that we have a proper validation response - if (validationResult && typeof validationResult === 'object' && 'is_valid' in validationResult) { - console.log('Valid validation response received:', validationResult); - return validationResult; + // Handle successful response - check for nested data + if ('data' in response) { + return response.data; + } + + // If response seems to be the validation result directly + if ('valid' in response) { + return response as DataValidation; } } - // If we get here, the response format is unexpected - console.warn('Unexpected response format:', response); - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: 'Respuesta inválida del servidor. Formato de respuesta no reconocido.' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'invalid_response', - message: 'Unexpected response format from validation service' - } - }; + throw new Error('Invalid response format from validation service'); } catch (error: any) { - console.error('Error in validateSalesData:', error); + console.error('Error validating file:', error); - // ✅ COMPREHENSIVE ERROR HANDLING + let errorMessage = 'Error al validar el archivo'; + if (error.response?.status === 422) { + errorMessage = 'Formato de archivo inválido'; + } else if (error.response?.status === 400) { + errorMessage = 'El archivo no se puede procesar'; + } else if (error.response?.status === 500) { + errorMessage = 'Error del servidor. Inténtalo más tarde.'; + } else if (error.message) { + errorMessage = error.message; + } - // Handle network/connection errors - if (error.code === 'NETWORK_ERROR' || error.message?.includes('fetch')) { - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: 'Error de conexión. Verifica tu conexión a internet y vuelve a intentar.' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'network_error', - message: 'Failed to connect to validation service' - } - }; - } - - // Handle HTTP status errors - if (error.response) { - const status = error.response.status; - console.log('HTTP error status:', status); - - if (status === 404) { - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: 'El servicio de validación no está disponible. Contacta al administrador del sistema.' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'service_not_found', - message: 'Validation endpoint not found (404)' - } - }; - } - - if (status === 422) { - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: error.response?.data?.detail || 'Formato de datos inválido' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'validation_failed', - message: 'Data validation failed' - } - }; - } - - if (status === 400) { - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: error.response?.data?.detail || 'Solicitud inválida' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'bad_request', - message: 'Bad request to validation service' - } - }; - } - - if (status >= 500) { - return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: 'Error interno del servidor. Inténtalo más tarde.' - }], - warnings: [], - summary: { - validation_error: true, - error_type: 'server_error', - message: 'Internal server error during validation' - } - }; - } - } - - // Handle any other unknown errors + // ✅ FIXED: Return format matching backend schema return { - is_valid: false, - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ - message: error.message || 'Error desconocido durante la validación' - }], + valid: false, + errors: [errorMessage], warnings: [], - summary: { - validation_error: true, - error_type: 'unknown_error', - message: error.message || 'Unknown validation error' - } + suggestions: ['Intenta con un archivo diferente o contacta soporte'] }; } } diff --git a/frontend/src/pages/onboarding.tsx b/frontend/src/pages/onboarding.tsx index b58bc01f..36148c8b 100644 --- a/frontend/src/pages/onboarding.tsx +++ b/frontend/src/pages/onboarding.tsx @@ -322,151 +322,140 @@ const OnboardingPage = () => { } }; - const handleFileUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; +// ✅ FIXED: Update handleFileUpload to use backend's schema +const handleFileUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; - setFormData(prev => ({ ...prev, salesFile: file })); - setUploadValidation(null); + setFormData(prev => ({ ...prev, salesFile: file })); + setUploadValidation(null); + + try { + setLoading(true); + console.log('Validating file:', file.name); - // Auto-validate file on selection - try { - setLoading(true); - console.log('Validating file:', file.name); - - // ✅ CRITICAL FIX: Add null check for validation response - const validation = await api.data.validateSalesData(file); - - if (!validation) { - throw new Error('No validation response received from server'); - } - - console.log('Validation result:', validation); // Debug log - - setUploadValidation(validation); - - // ✅ FIXED: Use correct field name is_valid instead of valid - if (validation.is_valid) { - showNotification('success', 'Archivo válido', - `${validation.total_records} registros detectados, ${validation.valid_records} válidos.`); - } else if (validation.warnings && validation.warnings.length > 0 && - validation.errors && validation.errors.length === 0) { - showNotification('warning', 'Archivo con advertencias', - 'El archivo es válido pero tiene algunas advertencias.'); - } else { - const errorCount = validation.errors ? validation.errors.length : 0; - showNotification('error', 'Archivo con errores', - `Se encontraron ${errorCount} errores en el archivo.`); - } - - } catch (error: any) { - console.error('Error validating file:', error); - - // Handle network or other errors - let errorMessage = 'Error al validar el archivo'; - if (error.response?.status === 422) { - errorMessage = 'Formato de archivo inválido'; - } else if (error.response?.status === 400) { - errorMessage = 'El archivo no se puede procesar'; - } else if (error.response?.status === 500) { - errorMessage = 'Error del servidor. Inténtalo más tarde.'; - } else if (error.message) { - errorMessage = error.message; - } - - showNotification('error', 'Error de validación', errorMessage); - - // ✅ FIXED: Set validation state with correct field names - setUploadValidation({ - is_valid: false, // ✅ Use is_valid not valid - total_records: 0, - valid_records: 0, - invalid_records: 0, - errors: [{ message: errorMessage }], // ✅ Array of objects, not strings - warnings: [], - summary: { validation_failed: true } - }); - } finally { - setLoading(false); + // Pass the current tenant ID to validation + const validation = await api.data.validateSalesData(file, currentTenantId); + + if (!validation) { + throw new Error('No validation response received from server'); } - }; + + console.log('Validation result:', validation); + setUploadValidation(validation); + + // ✅ FIXED: Use backend's "valid" field instead of "is_valid" + if (validation.valid) { + showNotification('success', 'Archivo válido', + `${validation.recordCount || 'Algunos'} registros detectados.`); + } else if (validation.warnings && validation.warnings.length > 0 && + validation.errors && validation.errors.length === 0) { + showNotification('warning', 'Archivo con advertencias', + 'El archivo es válido pero tiene algunas advertencias.'); + } else { + const errorCount = validation.errors ? validation.errors.length : 0; + showNotification('error', 'Archivo con errores', + `Se encontraron ${errorCount} errores en el archivo.`); + } + + } catch (error: any) { + console.error('Error validating file:', error); + + let errorMessage = 'Error al validar el archivo'; + if (error.response?.status === 422) { + errorMessage = 'Formato de archivo inválido'; + } else if (error.response?.status === 400) { + errorMessage = 'El archivo no se puede procesar'; + } else if (error.response?.status === 500) { + errorMessage = 'Error del servidor. Inténtalo más tarde.'; + } else if (error.message) { + errorMessage = error.message; + } + + showNotification('error', 'Error de validación', errorMessage); + + // ✅ FIXED: Set validation state using backend's schema + setUploadValidation({ + valid: false, + errors: [errorMessage], + warnings: [], + suggestions: ['Intenta con un archivo diferente'] + }); + } finally { + setLoading(false); + } +}; // Fixed validation display component const renderValidationResult = () => { - {uploadValidation && ( + if (!uploadValidation) return null; + + return (
- {uploadValidation.is_valid ? ( + {uploadValidation.valid ? ( ) : ( )}

- {uploadValidation.is_valid ? 'Archivo válido' : 'Archivo con problemas'} + {uploadValidation.valid ? 'Archivo válido' : 'Archivo con problemas'}

- {/* Always safe record count display */} + {/* ✅ FIXED: Display record count using backend's field names */}

- {uploadValidation.total_records || uploadValidation.recordCount || 0} registros procesados + {uploadValidation.recordCount || 0} registros encontrados + {uploadValidation.duplicates && uploadValidation.duplicates > 0 && + `, ${uploadValidation.duplicates} duplicados`}

- {/* Only show errors if they exist and are in array format */} - {!uploadValidation.is_valid && uploadValidation.errors && ( + {/* ✅ FIXED: Handle errors as string array (backend's current format) */} + {!uploadValidation.valid && uploadValidation.errors && uploadValidation.errors.length > 0 && (
-

- {Array.isArray(uploadValidation.errors) ? 'Errores encontrados:' : 'Error:'} -

- {Array.isArray(uploadValidation.errors) ? ( -
    - {uploadValidation.errors.map((error, idx) => ( -
  • - • {error?.message || error || 'Error no especificado'} -
  • - ))} -
- ) : ( -

- {uploadValidation.errors.message || uploadValidation.errors || 'Error desconocido'} -

- )} -
- )} - - {/* Show warnings if they exist */} - {uploadValidation.warnings && Array.isArray(uploadValidation.warnings) && uploadValidation.warnings.length > 0 && ( -
-

Advertencias:

-
    - {uploadValidation.warnings.map((warning, idx) => ( -
  • - • {warning?.message || warning || 'Advertencia no especificada'} -
  • +

    Errores encontrados:

    +
      + {uploadValidation.errors.map((error, idx) => ( +
    • • {error}
    • ))}
)} - {/* Show technical info for debugging */} - {uploadValidation.summary && uploadValidation.summary.error_type && ( -
-

- Info técnica: {uploadValidation.summary.error_type} -

+ {/* ✅ FIXED: Handle warnings as string array (backend's current format) */} + {uploadValidation.warnings && uploadValidation.warnings.length > 0 && ( +
+

Advertencias:

+
    + {uploadValidation.warnings.map((warning, idx) => ( +
  • • {warning}
  • + ))} +
+
+ )} + + {/* ✅ FIXED: Handle backend's "suggestions" field */} + {uploadValidation.suggestions && uploadValidation.suggestions.length > 0 && ( +
+

Sugerencias:

+
    + {uploadValidation.suggestions.map((suggestion, idx) => ( +
  • • {suggestion}
  • + ))} +
)}
- )} - }; - + ); +}; const renderStepIndicator = () => (
@@ -814,8 +803,10 @@ const OnboardingPage = () => {

Errores:

    - {uploadValidation.errors.map((error, idx) => ( -
  • • {error}
  • + {validation.errors.map((error, idx) => ( +
  • + • {typeof error === 'string' ? error : (error?.message || 'Error no especificado')} +
  • ))}
diff --git a/services/data/app/api/sales.py b/services/data/app/api/sales.py index 261b8df2..bebf089b 100644 --- a/services/data/app/api/sales.py +++ b/services/data/app/api/sales.py @@ -36,7 +36,7 @@ from shared.auth.decorators import ( get_current_tenant_id_dep ) -router = APIRouter(prefix="/sales", tags=["sales"]) +router = APIRouter(tags=["sales"]) logger = structlog.get_logger() @router.post("/", response_model=SalesDataResponse)