Skip to content

Latest commit

 

History

History
611 lines (507 loc) · 18.5 KB

File metadata and controls

611 lines (507 loc) · 18.5 KB

Location Validation Fix - Preventing Invalid Mover Records

Overview

Fixed a critical issue where the mover registration form allowed submission with invalid default coordinates (latitude: 0, longitude: 0), which could create invalid mover records in the database.

Problem

The primary_location field in the registration form was initialized with default coordinates of (0, 0), which:

  1. Creates invalid database records - Movers with coordinates (0, 0) don't have a real location
  2. Bypasses validation - The old validation only checked if (!latitude || !longitude) which passes for (0, 0)
  3. Causes service issues - Invalid coordinates can't be used for distance calculations or map display
  4. Poor user experience - Users could submit the form without selecting a real location

Solution Implemented

1. Made primary_location Optional in Type Definition

File: src/pages/MoverRegistration.tsx

Before:

export interface RegistrationData {
  // ...
  primary_location: {
    latitude: number;
    longitude: number;
    address: string;
  };
  // ...
}

After:

export interface RegistrationData {
  // ...
  primary_location?: {  // Made optional with ?
    latitude: number;
    longitude: number;
    address: string;
  };
  // ...
}

Why: Making it optional forces us to check for existence throughout the code and prevents accidental use of default values.

2. Removed Default (0, 0) Coordinates from Initial State

File: src/pages/MoverRegistration.tsx (lines 75-85)

Before:

const [registrationData, setRegistrationData] = useState<RegistrationData>({
  business_name: '',
  phone_primary: '',
  vehicle_types: [],
  has_helpers: false,
  helper_count: 0,
  service_radius_km: 10,
  primary_location: {
    latitude: 0,    // ❌ Invalid default
    longitude: 0,   // ❌ Invalid default
    address: ''
  },
  documents: {}
});

After:

const [registrationData, setRegistrationData] = useState<RegistrationData>({
  business_name: '',
  phone_primary: '',
  vehicle_types: [],
  has_helpers: false,
  helper_count: 0,
  service_radius_km: 10,
  // primary_location is undefined by default - must be set by user
  documents: {}
});

Why: No default coordinates means the field is truly empty until the user selects a location.

3. Added Comprehensive Location Validation in handleSubmit

File: src/pages/MoverRegistration.tsx (lines 110-157)

Before:

const handleSubmit = async () => {
  // Basic validation...
  
  if (
    !registrationData.primary_location.latitude ||
    !registrationData.primary_location.longitude
  ) {
    toast({
      title: 'Validation Error',
      description: 'Please set your primary location.',
      variant: 'destructive'
    });
    return;
  }
  // ... rest of submission
}

Problems with old validation:

  • Didn't check if primary_location exists (would crash with undefined)
  • !0 evaluates to true, so (0, 0) would pass validation
  • No type checking (could be strings or other invalid values)
  • No geographic bounds checking

After:

const handleSubmit = async () => {
  // Basic validation...
  
  // Comprehensive location validation
  if (!registrationData.primary_location) {
    toast({
      title: 'Location Required',
      description: 'Please select your primary operating location before submitting.',
      variant: 'destructive'
    });
    return;
  }

  const { latitude, longitude } = registrationData.primary_location;
  
  // Check that latitude and longitude are present and valid numbers
  if (
    typeof latitude !== 'number' ||
    typeof longitude !== 'number' ||
    isNaN(latitude) ||
    isNaN(longitude)
  ) {
    toast({
      title: 'Invalid Location',
      description: 'The selected location coordinates are invalid. Please select a valid location.',
      variant: 'destructive'
    });
    return;
  }

  // Check that coordinates are not default (0, 0) values
  if (latitude === 0 && longitude === 0) {
    toast({
      title: 'Invalid Location',
      description: 'Please select a real location. Default coordinates are not allowed.',
      variant: 'destructive'
    });
    return;
  }

  // Basic validation for realistic Kenya coordinates
  // Kenya roughly: lat -5 to 5, lng 34 to 42
  if (latitude < -5 || latitude > 5 || longitude < 33 || longitude > 43) {
    toast({
      title: 'Location Out of Range',
      description: 'Please select a location within Kenya.',
      variant: 'destructive'
    });
    return;
  }
  
  // ... rest of submission with non-null assertion
  primary_location: `POINT(${registrationData.primary_location!.longitude.toFixed(6)} ${registrationData.primary_location!.latitude.toFixed(6)})`,
}

Validation Stages:

  1. Existence Check - Ensures primary_location is not undefined
  2. Type Check - Ensures coordinates are actual numbers, not strings or null
  3. NaN Check - Catches NaN values that might slip through
  4. Zero Check - Explicitly rejects (0, 0) coordinates
  5. Geographic Bounds - Ensures location is within Kenya (rough bounds)
  6. User-Facing Errors - Clear toast messages for each error type

4. Updated ServiceAreaStep Validation Logic

File: src/components/mover-registration/ServiceAreaStep.tsx (lines 62-67)

Before:

const isValid = data.primary_location?.latitude !== 0 && data.primary_location?.longitude !== 0;

Problems:

  • undefined !== 0 evaluates to true (passes when location is missing)
  • Doesn't check for valid numbers
  • Doesn't catch NaN values

After:

// Check that primary_location exists and has valid, non-zero coordinates
const isValid =
  data.primary_location != null &&
  typeof data.primary_location.latitude === 'number' &&
  typeof data.primary_location.longitude === 'number' &&
  !isNaN(data.primary_location.latitude) &&
  !isNaN(data.primary_location.longitude) &&
  !(data.primary_location.latitude === 0 && data.primary_location.longitude === 0);

Comprehensive Checks:

  1. ✅ Object exists (!= null catches both null and undefined)
  2. ✅ Latitude is a number type
  3. ✅ Longitude is a number type
  4. ✅ Latitude is not NaN
  5. ✅ Longitude is not NaN
  6. ✅ Not the invalid (0, 0) coordinate pair

5. Added Next Button with Validation

File: src/components/mover-registration/ServiceAreaStep.tsx (lines 170-183)

Added:

{!isValid && (
  <div className="p-4 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg">
    <p className="text-sm text-red-600 dark:text-red-400">
      <strong>Location Required:</strong> Please select your primary operating location before proceeding.
    </p>
  </div>
)}

<div className="flex justify-end pt-4">
  <Button 
    onClick={onNext}
    disabled={!isValid}
    size="lg"
  >
    Next
  </Button>
</div>

Why:

  • ✅ Users cannot proceed to the next step without selecting a valid location
  • ✅ Clear visual feedback (disabled button + error message)
  • ✅ Consistent UX with other registration steps (BusinessInfoStep pattern)

6. Fixed Location Display

File: src/components/mover-registration/ServiceAreaStep.tsx (lines 113-124)

Before: Broken conditional rendering with orphaned tags

After:

{data.primary_location && (
  <div className="mt-4 p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg flex gap-3">
    <MapPin className="h-4 w-4 text-primary mt-0.5" />
    <div className="flex-1">
      <p className="text-sm font-medium">Selected Location</p>
      <p className="text-xs text-muted-foreground">
        {data.primary_location.address}
      </p>
      <p className="text-xs text-muted-foreground mt-1">
        {data.primary_location.latitude.toFixed(6)}, {data.primary_location.longitude.toFixed(6)}
      </p>
    </div>
  </div>
)}

Why: Only shows location display when a real location has been selected.

Validation Flow

User Journey

  1. Page Load

    • primary_location is undefined
    • Location display not shown (conditional rendering)
    • Next button is disabled
    • Error message visible: "Location Required"
  2. User Searches for Location

    • Uses LocationAutocomplete component
    • Selects a place from suggestions
    • handleLocationChange updates primary_location with real coordinates
  3. Location Selected

    • Location display appears (green box with coordinates)
    • isValid becomes true
    • Next button becomes enabled
    • Error message disappears
  4. User Clicks Next

    • Navigation to next step (Documents)
    • Can continue through registration
  5. Final Submit (Review Step)

    • handleSubmit runs comprehensive validation:
      • ✅ Checks primary_location exists
      • ✅ Validates coordinate types
      • ✅ Rejects (0, 0) coordinates
      • ✅ Checks geographic bounds (Kenya)
    • If valid: Creates mover record with PostGIS POINT geometry
    • If invalid: Shows user-facing error toast, prevents submission

Type Safety

Non-Null Assertion

Since we've added comprehensive validation before the database insert, we use TypeScript's non-null assertion operator (!) when building the PostGIS point:

primary_location: `POINT(${registrationData.primary_location!.longitude.toFixed(6)} ${registrationData.primary_location!.latitude.toFixed(6)})`

The ! tells TypeScript: "I've validated this is not null/undefined, trust me."

Why it's safe:

  • Multiple validation checks before this point
  • Early returns prevent reaching this code with invalid data
  • Clear user-facing errors if validation fails

Benefits

1. Data Integrity

  • No invalid records - Cannot create movers with (0, 0) coordinates
  • Geographic validation - Only Kenya locations accepted
  • Type safety - Only valid numbers accepted

2. User Experience

  • Clear feedback - Users know location is required
  • Progressive validation - Can't proceed without valid location
  • Visual indicators - Disabled button, error messages, success display
  • No silent failures - All validation errors show toast notifications

3. Service Reliability

  • Valid coordinates - All mover locations can be used for distance calculations
  • Map display - All locations can be shown on maps
  • Search accuracy - Location-based searches return correct results

4. Developer Experience

  • Type safety - Optional type forces existence checks
  • Clear validation logic - Easy to understand and maintain
  • Consistent patterns - Matches other form steps

Testing Recommendations

Manual Testing

  1. Empty Location Test

    • Load registration form
    • Navigate to Service Area step
    • Verify Next button is disabled
    • Verify error message is shown
    • Try to submit form - should be blocked
  2. Location Selection Test

    • Search for a Kenya location (e.g., "Nairobi")
    • Select from autocomplete
    • Verify location display appears with coordinates
    • Verify Next button becomes enabled
    • Verify error message disappears
  3. Current Location Test

    • Click "Use My Current Location" button
    • Grant location permission
    • Verify location updates with reverse geocoded address
    • Verify coordinates are shown
    • Verify can proceed to next step
  4. Geographic Bounds Test

    • Manually test with coordinates outside Kenya (if possible)
    • Should show "Location Out of Range" error on submit
  5. Complete Registration Test

    • Fill all required fields
    • Select valid Kenya location
    • Submit registration
    • Verify mover record created successfully
    • Check database - primary_location should be valid PostGIS POINT

Automated Testing

describe('ServiceAreaStep - Location Validation', () => {
  it('should disable Next button when location is not selected', () => {
    const { getByText } = render(
      <ServiceAreaStep 
        data={{ service_radius_km: 10 }} // No primary_location
        onUpdate={jest.fn()}
        onNext={jest.fn()}
      />
    );
    
    const nextButton = getByText('Next');
    expect(nextButton).toBeDisabled();
  });

  it('should show error message when location is missing', () => {
    const { getByText } = render(
      <ServiceAreaStep 
        data={{ service_radius_km: 10 }}
        onUpdate={jest.fn()}
        onNext={jest.fn()}
      />
    );
    
    expect(getByText(/Location Required/)).toBeInTheDocument();
  });

  it('should enable Next button when valid location is selected', () => {
    const { getByText } = render(
      <ServiceAreaStep 
        data={{ 
          service_radius_km: 10,
          primary_location: {
            latitude: -1.286389,
            longitude: 36.817223,
            address: 'Nairobi, Kenya'
          }
        }}
        onUpdate={jest.fn()}
        onNext={jest.fn()}
      />
    );
    
    const nextButton = getByText('Next');
    expect(nextButton).toBeEnabled();
  });

  it('should reject (0, 0) coordinates as invalid', () => {
    const { getByText } = render(
      <ServiceAreaStep 
        data={{ 
          service_radius_km: 10,
          primary_location: {
            latitude: 0,
            longitude: 0,
            address: ''
          }
        }}
        onUpdate={jest.fn()}
        onNext={jest.fn()}
      />
    );
    
    const nextButton = getByText('Next');
    expect(nextButton).toBeDisabled();
  });
});

describe('MoverRegistration - Location Validation', () => {
  it('should prevent submission without location', async () => {
    const { getByText } = render(<MoverRegistration />);
    
    // Fill required fields
    fireEvent.change(getByLabelText('Business Name'), { target: { value: 'Test Movers' } });
    fireEvent.change(getByLabelText('Phone'), { target: { value: '+254712345678' } });
    
    // Try to submit without location
    fireEvent.click(getByText('Submit'));
    
    await waitFor(() => {
      expect(getByText(/Location Required/)).toBeInTheDocument();
    });
  });

  it('should reject (0, 0) coordinates on submission', async () => {
    const mockSubmit = jest.fn();
    const { getByText } = render(
      <MoverRegistration 
        initialData={{
          business_name: 'Test Movers',
          phone_primary: '+254712345678',
          primary_location: { latitude: 0, longitude: 0, address: '' }
        }}
        onSubmit={mockSubmit}
      />
    );
    
    fireEvent.click(getByText('Submit'));
    
    await waitFor(() => {
      expect(getByText(/Default coordinates are not allowed/)).toBeInTheDocument();
      expect(mockSubmit).not.toHaveBeenCalled();
    });
  });

  it('should reject coordinates outside Kenya', async () => {
    const mockSubmit = jest.fn();
    const { getByText } = render(
      <MoverRegistration 
        initialData={{
          business_name: 'Test Movers',
          phone_primary: '+254712345678',
          primary_location: { 
            latitude: 40.7128, // New York
            longitude: -74.0060,
            address: 'New York, USA'
          }
        }}
        onSubmit={mockSubmit}
      />
    );
    
    fireEvent.click(getByText('Submit'));
    
    await waitFor(() => {
      expect(getByText(/within Kenya/)).toBeInTheDocument();
      expect(mockSubmit).not.toHaveBeenCalled();
    });
  });

  it('should accept valid Kenya location', async () => {
    const mockSubmit = jest.fn();
    const { getByText } = render(
      <MoverRegistration 
        initialData={{
          business_name: 'Test Movers',
          phone_primary: '+254712345678',
          vehicle_types: ['pickup_truck'],
          primary_location: { 
            latitude: -1.286389,
            longitude: 36.817223,
            address: 'Nairobi, Kenya'
          }
        }}
        onSubmit={mockSubmit}
      />
    );
    
    fireEvent.click(getByText('Submit'));
    
    await waitFor(() => {
      expect(mockSubmit).toHaveBeenCalled();
    });
  });
});

Files Changed

  1. src/pages/MoverRegistration.tsx

    • Lines 41-47: Made primary_location optional in interface
    • Lines 75-85: Removed default (0, 0) coordinates from initial state
    • Lines 110-157: Added comprehensive location validation in handleSubmit
    • Line 221: Added non-null assertion for database insert
  2. src/components/mover-registration/ServiceAreaStep.tsx

    • Lines 58-61: Fixed duplicate error handler
    • Lines 62-67: Enhanced isValid check with comprehensive validation
    • Lines 113-124: Fixed conditional rendering for location display
    • Lines 170-183: Added Next button with validation and error message

Migration Notes

Database Impact

  • No migration required - Existing records unchanged
  • No breaking changes - Column type (geography(Point, 4326)) remains the same
  • Future records - All new mover registrations will have valid locations

Existing Invalid Records

If there are existing movers with (0, 0) coordinates in the database:

-- Find invalid mover records
SELECT 
  id, 
  business_name, 
  ST_AsText(primary_location) as location
FROM movers
WHERE ST_X(primary_location::geometry) = 0 
  AND ST_Y(primary_location::geometry) = 0;

-- Mark them for manual review
UPDATE movers
SET verification_status = 'needs_review'
WHERE ST_X(primary_location::geometry) = 0 
  AND ST_Y(primary_location::geometry) = 0;

Related Documentation

Verification

TypeScript Compilation: Passed with no errors ✅ Type Safety: primary_location properly optional, all accesses checked ✅ Validation Logic: Comprehensive checks at both UI and submission levels ✅ User Experience: Clear feedback, cannot proceed without valid location ✅ Data Integrity: Invalid coordinates cannot reach database ✅ Geographic Bounds: Only Kenya locations accepted

Kenya Geographic Bounds Reference

For reference, Kenya's approximate bounding box:

  • Latitude: -5° to 5° (spans equator)
  • Longitude: 34° to 42°
  • Major Cities:
    • Nairobi: -1.286389, 36.817223
    • Mombasa: -4.043477, 39.668206
    • Kisumu: -0.091702, 34.767956
    • Nakuru: -0.303099, 36.080025

The validation uses slightly expanded bounds to avoid edge cases:

  • Latitude: -5 to 5
  • Longitude: 33 to 43

This ensures all of Kenya is included while rejecting clearly invalid locations.