Jake Vanderwerf
2025-11-25 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850
inc/integrations/GoogleMyBusiness.php
@@ -10,9 +10,12 @@
class GoogleMyBusiness extends Integrations
{
   private string $access_token;
   private ?string $access_token = null;
   protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
   private ?string $location = null;
   private ?string $refresh_token = null;
   private ?string $client_id = null;
   private ?string $client_secret = null;
   private ?string $account_id = null;
   public function __construct(?int $userID = null)
@@ -35,6 +38,7 @@
      ];
      $this->apiEndpoints = [
         '/accounts/[^/]+/locations/[^/]+/reviews',
         '/accounts/[^/]+/locations/[^/]+/foodMenus',
         '/v4/accounts/[^/]+/locations/[^/]+/media',
         '/v4/accounts/[^/]+/locations/[^/]+/localPosts',
@@ -81,18 +85,18 @@
            'label'  => 'OAuth Client Secret',
            'required'  => true,
         ],
         'access_token' => [
            'type'   => 'text',
            'subtype'   => 'password',
            'label'  => 'Access Token',
            'hint'   => 'Generated automagically after OAuth authorization.'
         ],
         'refresh_token'   => [
            'type'   => 'text',
            'subtype'   => 'password',
            'label'  => 'Refresh Token',
            'hint'   => 'Generated automagically after OAuth authorization.'
         ]
//       'access_token' => [
//          'type'   => 'text',
//          'subtype'   => 'password',
//          'label'  => 'Access Token',
//          'hint'   => 'Generated automagically after OAuth authorization.'
//       ],
//       'refresh_token'   => [
//          'type'   => 'text',
//          'subtype'   => 'password',
//          'label'  => 'Refresh Token',
//          'hint'   => 'Generated automagically after OAuth authorization.'
//       ]
      ];
      $this->advanced = [
@@ -124,6 +128,8 @@
            'check_oauth_status' => 'Check OAuth Status'
         ]
      );
//    $this->cache->clear();
   }
   protected function initialize(): void
@@ -131,12 +137,12 @@
      if (empty($this->credentials)) {
         $this->loadCredentials();
      }
      $this->access_token = $this->credentials['access_token'] ?? '';
      $this->refresh_token = $this->credentials['refresh_token'] ?? '';
      $this->client_id = $this->credentials['client_id'] ?? '';
      $this->client_secret = $this->credentials['client_secret'] ?? '';
      $this->location = $this->credentials['location'] ?? null;
      $this->account_id = $this->credentials['account'] ?? null;
      $this->access_token = (array_key_exists('access_token', $this->credentials)) ? $this->credentials['access_token'] : null;
      $this->refresh_token = (array_key_exists('refresh_token', $this->credentials)) ? $this->credentials['refresh_token'] : null;
      $this->client_id = (array_key_exists('client_id', $this->credentials)) ? $this->credentials['client_id'] : null;
      $this->client_secret = (array_key_exists('client_secret', $this->credentials)) ? $this->credentials['client_secret'] : null;
      $this->location = (array_key_exists('location', $this->credentials)) ? $this->credentials['location'] : null;
      $this->account_id = (array_key_exists('account', $this->credentials)) ? $this->credentials['account'] : null;
      if ($this->account_id) {
         $this->apiEndpoints[] = "/v1/{$this->account_id}/locations";
@@ -157,6 +163,16 @@
      }
   }
   /**
    * Check if response contains an error - Google-specific
    */
   protected function isErrorResponse(array $response): bool
   {
      // Google APIs return errors in this format:
      // {"error": {"code": 401, "message": "...", "status": "UNAUTHENTICATED"}}
      return isset($response['error']) && isset($response['error']['code']);
   }
   protected function getRequestHeaders(): array
   {
      return [
@@ -1431,6 +1447,128 @@
   }
   /**
    * Get reviews for the current location
    * @param int $page_size Number of reviews to fetch (max 50)
    * @return array|null
    */
   public function getReviews(int $page_size = 5): ?array
   {
      $this->ensureInitialized();
      if (!$this->location) {
         throw new \Exception('No location selected');
      }
      if (!$this->account_id) {
         throw new \Exception('No account configured');
      }
      $location = $this->getSelectedLocationResourceName();
      $account = $this->account_id;
      // Check cache first (weekly refresh = 604800 seconds)
      $cache_key = ['reviews', $location, $page_size];
      $cached = $this->cache->get($cache_key);
      if ($cached !== false) {
         return $cached;
      }
      try {
         // Reviews endpoint from My Business Account Management API
         $response = $this->getRequest(
            "/{$account}/{$location}/reviews",
            [
               'orderBy' => 'updateTime desc'
            ],
            'v4'
         );
         error_log('Review response: '.print_r($response, true));
         $reviews = $response ?? [];
         // Cache for 1 week (604800 seconds)
         $this->cache->set($cache_key, $reviews, WEEK_IN_SECONDS);
         return $reviews;
      } catch (\Exception $e) {
         $this->logError($e->getMessage(), [
            'method' => 'getReviews'
         ]);
         return null;
      }
   }
   /**
    * Get the URL to view all Google reviews for the current location
    * @return string|null The reviews viewing URL or null if not available
    */
   public function getReviewsViewUrl(): ?string
   {
      $this->ensureInitialized();
      try {
         $location = $this->getLocation();
         if (empty($location)) {
            return null;
         }
         // Prefer maps URL as it shows all reviews directly
         if (!empty($location['metadata']['mapsUrl'])) {
            return $location['metadata']['mapsUrl'];
         }
         // Fallback: construct from Place ID
         if (!empty($location['metadata']['placeId'])) {
            return 'https://search.google.com/local/reviews?placeid=' .
               urlencode($location['metadata']['placeId']);
         }
         return null;
      } catch (\Exception $e) {
         $this->logError('Failed to get reviews view URL: ' . $e->getMessage(), [
            'method' => 'getReviewsViewUrl'
         ]);
         return null;
      }
   }
   /**
    * Get the URL to leave a review for the current location
    * @return string|null The review URL or null if not available
    */
   public function getReviewUrl(): ?string
   {
      $this->ensureInitialized();
      try {
         $location = $this->getLocation();
         if (empty($location)) {
            return null;
         }
         // Try to use Place ID for write review
         if (!empty($location['metadata']['placeId'])) {
            return 'https://search.google.com/local/writereview?placeid=' .
               urlencode($location['metadata']['placeId']);
         }
         // Fallback to maps URL
         if (!empty($location['metadata']['mapsUrl'])) {
            return $location['metadata']['mapsUrl'] . '/reviews';
         }
         return null;
      } catch (\Exception $e) {
         $this->logError('Failed to get review URL: ' . $e->getMessage(), [
            'method' => 'getReviewUrl'
         ]);
         return null;
      }
   }
   /**
    * Get locations for an account (with persistent storage)
    * Allowed Fields: https://developers.google.com/my-business/content/location-data#list_of_all_supported_filter_fields
    */
@@ -2884,4 +3022,6 @@
         ]
      ];
   }
}