← back to Jill Website
src/services/googleMapsService.ts
267 lines
import axios from 'axios';
import dotenv from 'dotenv';
import { googleReviewsService } from './googleReviewsService';
dotenv.config();
export interface GoogleMapsConfig {
apiKey: string;
}
export interface PlaceDetails {
name: string;
address: string;
phone?: string;
website?: string;
rating?: number;
reviewLink?: string;
placeId: string;
}
export interface MapLinkOptions {
address?: string;
placeId?: string;
businessName?: string;
}
export interface GoogleMapsError {
code: string;
message: string;
status?: string;
}
/**
* Google Maps Service
* Provides integration with Google Maps API for generating map links
* and retrieving business information
*/
export class GoogleMapsService {
private apiKey: string;
private baseUrl: string = 'https://maps.googleapis.com/maps/api';
constructor(config?: GoogleMapsConfig) {
this.apiKey = config?.apiKey || process.env.GOOGLE_MAPS_API_KEY || '';
if (!this.apiKey) {
console.warn('Warning: Google Maps API key is not configured. Set GOOGLE_MAPS_API_KEY in environment variables.');
}
}
/**
* Verify that the API key is configured
*/
isConfigured(): boolean {
return this.apiKey.length > 0;
}
/**
* Generate a Google Maps link from address or place information
* @param options - Map link options containing address, placeId, or business name
* @returns Google Maps URL
*/
generateMapLink(options: MapLinkOptions): string {
try {
if (!options.address && !options.placeId && !options.businessName) {
throw new Error('At least one of address, placeId, or businessName must be provided');
}
// If we have a placeId, use the direct place link
if (options.placeId) {
return `https://www.google.com/maps/place/?q=place_id:${options.placeId}`;
}
// Otherwise, construct a search query
let query = '';
if (options.businessName && options.address) {
query = `${options.businessName}, ${options.address}`;
} else if (options.businessName) {
query = options.businessName;
} else if (options.address) {
query = options.address;
}
// URL encode the query
const encodedQuery = encodeURIComponent(query);
return `https://www.google.com/maps/search/?api=1&query=${encodedQuery}`;
} catch (error) {
console.error('Error generating map link:', error);
throw this.handleError(error);
}
}
/**
* Search for a place and get its details using Google Places API
* @param searchQuery - Business name and/or address to search for
* @returns Place details including placeId, address, phone, website
*/
async searchPlace(searchQuery: string): Promise<PlaceDetails | null> {
if (!this.isConfigured()) {
throw new Error('Google Maps API key is not configured');
}
try {
// First, find the place using Text Search
const searchUrl = `${this.baseUrl}/place/textsearch/json`;
const searchResponse = await axios.get(searchUrl, {
params: {
query: searchQuery,
key: this.apiKey
},
timeout: 10000 // 10 second timeout
});
if (searchResponse.data.status !== 'OK' && searchResponse.data.status !== 'ZERO_RESULTS') {
throw new Error(`Google Maps API error: ${searchResponse.data.status} - ${searchResponse.data.error_message || 'Unknown error'}`);
}
if (searchResponse.data.results.length === 0) {
console.log(`No results found for query: ${searchQuery}`);
return null;
}
// Get the first result's place_id
const placeId = searchResponse.data.results[0].place_id;
// Now get detailed information about the place
const detailsUrl = `${this.baseUrl}/place/details/json`;
const detailsResponse = await axios.get(detailsUrl, {
params: {
place_id: placeId,
fields: 'name,formatted_address,formatted_phone_number,website,rating,place_id',
key: this.apiKey
},
timeout: 10000
});
if (detailsResponse.data.status !== 'OK') {
throw new Error(`Google Maps API error: ${detailsResponse.data.status} - ${detailsResponse.data.error_message || 'Unknown error'}`);
}
const result = detailsResponse.data.result;
// Generate review link
const reviewLink = googleReviewsService.generateReviewLink(result.place_id);
return {
name: result.name,
address: result.formatted_address,
phone: result.formatted_phone_number,
website: result.website,
rating: result.rating,
reviewLink: reviewLink,
placeId: result.place_id
};
} catch (error) {
console.error('Error searching for place:', error);
throw this.handleError(error);
}
}
/**
* Get place details by Place ID
* @param placeId - Google Place ID
* @returns Place details
*/
async getPlaceById(placeId: string): Promise<PlaceDetails | null> {
if (!this.isConfigured()) {
throw new Error('Google Maps API key is not configured');
}
try {
const url = `${this.baseUrl}/place/details/json`;
const response = await axios.get(url, {
params: {
place_id: placeId,
fields: 'name,formatted_address,formatted_phone_number,website,rating,place_id',
key: this.apiKey
},
timeout: 10000
});
if (response.data.status !== 'OK' && response.data.status !== 'NOT_FOUND') {
throw new Error(`Google Maps API error: ${response.data.status} - ${response.data.error_message || 'Unknown error'}`);
}
if (response.data.status === 'NOT_FOUND') {
console.log(`Place not found with ID: ${placeId}`);
return null;
}
const result = response.data.result;
// Generate review link
const reviewLink = googleReviewsService.generateReviewLink(result.place_id);
return {
name: result.name,
address: result.formatted_address,
phone: result.formatted_phone_number,
website: result.website,
rating: result.rating,
reviewLink: reviewLink,
placeId: result.place_id
};
} catch (error) {
console.error('Error getting place by ID:', error);
throw this.handleError(error);
}
}
/**
* Handle and format errors consistently
*/
private handleError(error: unknown): Error {
if (axios.isAxiosError(error)) {
if (error.response) {
// API responded with error
const status = error.response.data?.status || 'UNKNOWN';
const message = error.response.data?.error_message || error.message;
return new Error(`Google Maps API error: ${status} - ${message}`);
} else if (error.request) {
// Request made but no response
return new Error('Google Maps API request failed: No response received');
} else {
// Error in request setup
return new Error(`Google Maps API request error: ${error.message}`);
}
}
if (error instanceof Error) {
return error;
}
return new Error('Unknown error occurred while accessing Google Maps API');
}
/**
* Verify API connection and key validity
* @returns true if API is accessible and key is valid
*/
async verifyConnection(): Promise<boolean> {
if (!this.isConfigured()) {
console.error('Google Maps API key is not configured');
return false;
}
try {
// Try a simple geocode request to verify the key works
const url = `${this.baseUrl}/geocode/json`;
const response = await axios.get(url, {
params: {
address: 'Nosara, Costa Rica',
key: this.apiKey
},
timeout: 5000
});
return response.data.status === 'OK' || response.data.status === 'ZERO_RESULTS';
} catch (error) {
console.error('Google Maps API connection verification failed:', error);
return false;
}
}
}
// Export singleton instance
export const googleMapsService = new GoogleMapsService();