Skip to main content

Document Upload System

Viax requires drivers to upload photos of official documents for verification. All uploads are secure, organized by driver, and reviewed by our admin team.
Documents are stored securely on the server with automatic cleanup of outdated files. Only the latest version is kept active.

Required Documents

Driver's License

Front and back photos of your valid driver’s license

SOAT Insurance

Mandatory vehicle insurance certificate photo

Tecnomecánica

Technical inspection certificate photo

Vehicle Ownership

Tarjeta de Propiedad (ownership card) photo

Upload Process

Step 1: Document Selection

The app provides an easy interface to select photos:
Photo Upload Widget
Widget _buildPhotoUpload({
  required String label,
  required String? photoPath,
  required VoidCallback onTap,
}) {
  final hasPhoto = photoPath != null;
  
  return GestureDetector(
    onTap: onTap,
    child: Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: hasPhoto ? Colors.green.withOpacity(0.1) : Colors.grey[100],
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: hasPhoto ? Colors.green : Colors.grey[300],
          width: 2,
        ),
      ),
      child: Row(
        children: [
          Icon(
            hasPhoto ? Icons.check_circle : Icons.camera_alt,
            color: hasPhoto ? Colors.green : Colors.grey[600],
          ),
          SizedBox(width: 12),
          Expanded(
            child: Text(
              label,
              style: TextStyle(
                fontSize: 16,
                fontWeight: hasPhoto ? FontWeight.bold : FontWeight.normal,
              ),
            ),
          ),
          if (hasPhoto)
            Text('Selected ✓', style: TextStyle(color: Colors.green)),
        ],
      ),
    ),
  );
}

Step 2: Image Selection Method

Choose between camera or gallery:
Take a photo directly with your phone’s camera
Take Photo
Future<void> _pickImageFromCamera(String documentType) async {
  final ImagePicker picker = ImagePicker();
  
  final XFile? image = await picker.pickImage(
    source: ImageSource.camera,
    maxWidth: 1920,
    maxHeight: 1920,
    imageQuality: 85,
  );
  
  if (image != null) {
    setState(() {
      switch(documentType) {
        case 'soat':
          _soatFotoPath = image.path;
          break;
        case 'tecnomecanica':
          _tecnomecanicaFotoPath = image.path;
          break;
        case 'tarjeta_propiedad':
          _tarjetaPropiedadFotoPath = image.path;
          break;
      }
    });
  }
}
Image Quality Requirements:
  • Maximum size: 5MB per photo
  • Recommended resolution: 1920x1920 pixels
  • Automatic compression to 85% quality
  • Supported formats: JPG, PNG, WEBP, PDF

Step 3: Document Upload Service

The DocumentUploadService handles secure uploads:
Document Upload Service
class DocumentUploadService {
  static const String baseUrl = 'http://76.13.114.194';
  static const String uploadEndpoint = '$baseUrl/conductor/upload_documents.php';
  
  /// Upload a single document
  static Future<String?> uploadDocument({
    required int conductorId,
    required String tipoDocumento,
    required String imagePath,
  }) async {
    try {
      var request = http.MultipartRequest('POST', Uri.parse(uploadEndpoint));
      
      // Add form fields
      request.fields['conductor_id'] = conductorId.toString();
      request.fields['tipo_documento'] = tipoDocumento;
      
      // Add file
      var file = await http.MultipartFile.fromPath(
        'documento',
        imagePath,
        contentType: MediaType('image', 'jpeg'),
      );
      request.files.add(file);
      
      // Send request
      var response = await request.send();
      var responseData = await response.stream.bytesToString();
      var jsonResponse = json.decode(responseData);
      
      if (jsonResponse['success'] == true) {
        return jsonResponse['data']['url'];
      } else {
        throw Exception(jsonResponse['message'] ?? 'Upload failed');
      }
    } catch (e) {
      print('Upload error: $e');
      return null;
    }
  }
  
  /// Upload multiple documents at once
  static Future<Map<String, String?>> uploadMultipleDocuments({
    required int conductorId,
    required Map<String, String> documents,
  }) async {
    Map<String, String?> results = {};
    
    for (var entry in documents.entries) {
      final url = await uploadDocument(
        conductorId: conductorId,
        tipoDocumento: entry.key,
        imagePath: entry.value,
      );
      results[entry.key] = url;
    }
    
    return results;
  }
}

Document Types & Validation

Driver’s License

licencia
DriverLicense
required
Driver’s license with front and back photos
License Upload
// Upload license photo
final licenseUrl = await DocumentUploadService.uploadDocument(
  conductorId: conductorId,
  tipoDocumento: 'licencia',
  imagePath: licenseFotoPath,
);

// Update license in profile
final license = DriverLicense(
  numero: licenseNumber,
  categoria: LicenseCategory.c1,
  fechaExpedicion: issueDate,
  fechaVencimiento: expiryDate,
  foto: licenseUrl,
  isVerified: false, // Will be verified by admin
);
Validation checks:
  • ✅ License number is not empty
  • ✅ Category is C1, C2, or C3 (public service)
  • ✅ Expiry date is in the future
  • ✅ Not expiring within 30 days

SOAT (Vehicle Insurance)

soat
object
required
Mandatory vehicle insurance certificate
Vehicle Model - SOAT
class VehicleModel {
  final String? soatNumero;
  final DateTime? soatVencimiento;
  final String? fotoSoat;
  
  bool get isSoatValid {
    if (soatVencimiento == null) return false;
    return soatVencimiento!.isAfter(DateTime.now());
  }
  
  bool get isSoatExpiringSoon {
    if (soatVencimiento == null) return false;
    final daysUntilExpiry = soatVencimiento!.difference(DateTime.now()).inDays;
    return daysUntilExpiry <= 30 && daysUntilExpiry > 0;
  }
}
Validation checks:
  • ✅ SOAT number provided
  • ✅ Expiry date is valid and in the future
  • ✅ Photo uploaded and visible

Tecnomecánica (Technical Inspection)

tecnomecanica
object
required
Vehicle technical inspection certificate
Tecnomecánica Validation
final String? tecnomecanicaNumero;
final DateTime? tecnomecanicaVencimiento;
final String? fotoTecnomecanica;

bool get isTecnomecanicaValid {
  return tecnomecanicaNumero != null &&
         tecnomecanicaNumero!.isNotEmpty &&
         tecnomecanicaVencimiento != null &&
         tecnomecanicaVencimiento!.isAfter(DateTime.now());
}

Tarjeta de Propiedad (Ownership Card)

tarjeta_propiedad
object
required
Vehicle ownership registration card
Ownership Card
final String? tarjetaPropiedadNumero;
final String? fotoTarjetaPropiedad;

bool get isOwnershipComplete {
  return tarjetaPropiedadNumero != null &&
         tarjetaPropiedadNumero!.isNotEmpty &&
         fotoTarjetaPropiedad != null &&
         fotoTarjetaPropiedad!.isNotEmpty;
}

Upload Workflow Example

Complete example of uploading vehicle documents:
1

Collect Document Paths

User selects all required photos
String? _soatFotoPath;
String? _tecnomecanicaFotoPath;
String? _tarjetaPropiedadFotoPath;
2

Upload Documents

Upload all documents before saving vehicle
Upload Documents
final uploadResults = await provider.uploadVehicleDocuments(
  conductorId: conductorId,
  soatFotoPath: _soatFotoPath,
  tecnomecanicaFotoPath: _tecnomecanicaFotoPath,
  tarjetaPropiedadFotoPath: _tarjetaPropiedadFotoPath,
);

if (uploadResults.containsValue(null)) {
  // Some uploads failed
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('Some documents failed to upload'))
  );
  return;
}
3

Save Vehicle with URLs

Create vehicle with uploaded document URLs
Save Vehicle
final vehicle = VehicleModel(
  placa: plateNumber,
  tipo: VehicleType.auto,
  marca: brand,
  modelo: model,
  anio: year,
  color: color,
  soatNumero: soatNumber,
  soatVencimiento: soatExpiry,
  fotoSoat: uploadResults['soat'],
  tecnomecanicaNumero: techNumber,
  tecnomecanicaVencimiento: techExpiry,
  fotoTecnomecanica: uploadResults['tecnomecanica'],
  tarjetaPropiedadNumero: ownershipNumber,
  fotoTarjetaPropiedad: uploadResults['tarjeta_propiedad'],
);

await provider.updateVehicle(
  conductorId: conductorId,
  vehicle: vehicle,
);
4

Show Success

Display confirmation message
if (success) {
  showDialog(
    context: context,
    builder: (context) => ApprovalSuccessDialog(
      message: 'Vehicle registered successfully!'
    ),
  );
}

Server-Side Processing

File Storage Structure

Documents are organized on the server:
viax/backend/uploads/documentos/
├── conductor_1/
   ├── soat_1730000000_a1b2c3d4.jpg
   ├── tecnomecanica_1730000000_e5f6g7h8.jpg
   └── tarjeta_propiedad_1730000000_i9j0k1l2.jpg
├── conductor_7/
   ├── licencia_1730000000_m3n4o5p6.jpg
   └── soat_1730000000_q7r8s9t0.jpg
└── conductor_15/
    └── ...

Upload Endpoint Response

{
  "success": true,
  "message": "Document uploaded successfully",
  "data": {
    "tipo_documento": "soat",
    "url": "uploads/documentos/conductor_7/soat_1730000000_a1b2c3d4.jpg",
    "conductor_id": 7,
    "fecha_subida": "2025-10-25 15:30:00"
  }
}

Security & Privacy

Server performs strict validation:
  • File type verification (MIME type check)
  • Size limit enforcement (max 5MB)
  • Only allowed formats: JPG, PNG, WEBP, PDF
  • Conductor existence verification
  • Files stored outside web root when possible
  • Unique filenames prevent overwrites
  • .htaccess protection against PHP execution
  • Automatic cleanup of old documents
  • Only authenticated conductors can upload
  • Conductors can only access their own documents
  • Admin approval required before activation

Document History

The system maintains a history of all uploaded documents:
CREATE TABLE documentos_conductor_historial (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  conductor_id BIGINT UNSIGNED NOT NULL,
  tipo_documento VARCHAR(50) NOT NULL,
  url_documento VARCHAR(500) NOT NULL,
  activo TINYINT(1) DEFAULT 1,
  fecha_subida TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  reemplazado_en TIMESTAMP NULL,
  FOREIGN KEY (conductor_id) REFERENCES usuarios(id)
);
  • When a new document is uploaded, the old one is marked as activo = 0
  • The reemplazado_en timestamp is set
  • Old files are deleted from the server

Troubleshooting

Possible causes:
  • File too large (over 5MB)
  • Invalid file format
  • Network connection issue
  • Server permissions problem
Solution:
  • Reduce image quality/size
  • Use JPG format
  • Check internet connection
  • Try again later
Common reasons:
  • Photo is blurry or unreadable
  • Document is expired
  • Information doesn’t match profile
  • Wrong document uploaded
Solution:
  • Take a clear, well-lit photo
  • Ensure all text is readable
  • Verify dates are valid
  • Upload the correct document
Best Practices:
  • Take photos in good lighting
  • Ensure all corners of the document are visible
  • Avoid shadows and glare
  • Use a flat surface as background
  • Verify all text is readable before uploading

Next Steps

Vehicle Management

Complete vehicle registration and details

Profile Management

Update your driver profile settings

Build docs developers (and LLMs) love