All files / src/services geocodingService.server.ts

52.38% Statements 77/147
100% Branches 13/13
62.5% Functions 5/8
61.53% Lines 64/104

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 1052x 2x 2x 2x 2x 2x 2x 2x 2x 2x 36x 36x 2x 2x 2x 2x 2x 2x 9x 2x 9x 9x 8x 2x 2x 2x 2x 2x 2x 2x 8x 7x 7x 5x 4x 4x   1x         2x           1x         4x 4x 3x 3x     1x 1x               7x 7x   1x         3x 3x 3x 3x   3x 3x 4x 3x 3x 2x 2x       2x 2x   1x 1x         24x        
import type { Logger } from 'pino';
import { connection as redis } from './queueService.server';
import { googleGeocodingService, GoogleGeocodingService } from './googleGeocodingService.server';
import {
  nominatimGeocodingService,
  NominatimGeocodingService,
} from './nominatimGeocodingService.server';
 
export class GeocodingService {
  constructor(
    private googleService: GoogleGeocodingService,
    private nominatimService: NominatimGeocodingService,
  ) {}
 
  async geocodeAddress(
    address: string,
    logger: Logger,
  ): Promise<{ lat: number; lng: number } | null> {
    const cacheKey = `geocode:${address}`;
 
    try {
      const cached = await redis.get(cacheKey);
      if (cached) {
        logger.info({ cacheKey }, 'Geocoding result found in cache.');
        return JSON.parse(cached);
      }
    } catch (error) {
      logger.error({ err: error, cacheKey }, 'Redis GET or JSON.parse command failed. Proceeding without cache.');
    }
 
    if (process.env.GOOGLE_MAPS_API_KEY) {
      try {
        const coordinates = await this.googleService.geocode(address, logger);
        if (coordinates) {
          await this.setCache(cacheKey, coordinates, logger);
          return coordinates;
        }
        logger.info(
          { address, provider: 'Google' },
          'Google Geocoding returned no results. Falling back to Nominatim.',
        );
      } catch (error) {
        logger.error(
          { err: error },
          'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.'
        );
      }
    } else {
      logger.warn(
        'GOOGLE_MAPS_API_KEY is not set. Falling back to Nominatim as the primary geocoding provider.',
      );
    }

    const nominatimResult = await this.nominatimService.geocode(address, logger);
    if (nominatimResult) {
      await this.setCache(cacheKey, nominatimResult, logger);
      return nominatimResult;
    }

    logger.error({ address }, 'All geocoding providers failed.');
    return null;
  }

  private async setCache(
    cacheKey: string,
    result: { lat: number; lng: number },
    logger: Logger,
  ): Promise<void> {
    try {
      await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
    } catch (error) {
      logger.error({ err: error, cacheKey }, 'Redis SET command failed. Result will not be cached.');
    }
  }

  async clearGeocodeCache(logger: Logger): Promise<number> {
    let cursor = '0';
    let totalDeleted = 0;
    const pattern = 'geocode:*';
    logger.info(`Starting geocode cache clear with pattern: ${pattern}`);

    try {
      do {
        const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
        cursor = nextCursor;
        if (keys.length > 0) {
          const deletedCount = await redis.del(keys);
          totalDeleted += deletedCount;
        }
      } while (cursor !== '0');

      logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
      return totalDeleted;
    } catch (error) {
      logger.error({ err: error }, 'Failed to clear geocode cache from Redis.');
      throw error;
    }
  }
}

export const geocodingService = new GeocodingService(
  googleGeocodingService,
  nominatimGeocodingService,
);