Jake Vanderwerf
2026-08-26 ccf5e61a075c35d24befc175994b5cb8957ca322
inc/integrations/services/GoogleMyBusiness.php
@@ -2,13 +2,16 @@
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Admin;
use JVBase\integrations\Instance;
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\integrations\OAuth;
use JVBase\integrations\OAuthCredentials;
use JVBase\integrations\OAuthURLs;
use JVBase\integrations\Requests;
use JVBase\integrations\SyncTo;
use JVBase\integrations\SyncToPost;
use JVBase\integrations\UserConnection;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -17,10 +20,10 @@
class GoogleMyBusiness extends Integrations
{
   use Admin, Instance, OAuth, Requests, UserConnection;
   use Admin, Instance, OAuth, Requests, UserConnection, SyncToPost;
   protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
   protected string $businessV4;
   protected function __construct()
   {
      $this->service_name = 'gmb';
@@ -34,6 +37,9 @@
      ];
      $this->supportsWebp = false;
      $this->baseUrl = 'https://mybusinessbusinessinformation.googleapis.com';
      $this->businessV4 = 'https://mybusiness.googleapis.com/v4';
      $this->defineFields();
      $this->defineInstructions();
      $this->defineOAuth();
@@ -139,7 +145,12 @@
   protected function getRequestHeaders(): array
   {
      // TODO: Implement getRequestHeaders() method.
      return [
         'Authorization' => 'Bearer ' . $this->getAccessToken(),
         'Content-Type' => 'application/json',
         'Accept' => 'application/json'
      ];
   }
   protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
@@ -150,14 +161,16 @@
      return match($endpoint) {
         'https://mybusinessaccountmanagement.googleapis.com/v1/accounts' => $this->handleGetAccountsResponse($response, $method),
         sprintf('%s/%s/%s/reviews', $this->businessV4, $this->getAccountID()??'', $this->getLocationID()??'') => $this->handleGetReviewResponse($response, $method),
         sprintf('https://mybusinessbusinessinformation.googleapis.com/v1/%s/locations', $this->getAccountID()??'') => $this->handleGetLocationsResponse($response, $method),
         sprintf('%s/v1/%s', $this->baseUrl,$this->getLocationID()??'') => $this->handleGetLocationResponse($response, $method),
         default => $this->response(false, 'Unhandled response for '.$endpoint.' endpoint, with method: '.$method)
      };
   }
   protected function refreshAccessToken(OAuthCredentials $credentials): array
   {
      // TODO: Implement refreshAccessToken() method.
   }
   /*********************************************************
@@ -176,6 +189,31 @@
      return $stored;
   }
   protected function getLocation(bool $force = false):?array
   {
      $location_name = $this->getLocationID();
      if (empty($location_name)) {
         return null;
      }
      $params = [
         'readMask'  => $this->readMask
      ];
      return $this->getRequest(
         sprintf(
            '%s/v1/%s',
            $this->baseUrl,
            $location_name,
         ),
         $params,
         $force
      );
   }
      protected function handleGetLocationResponse(WP_Error|array $response, string $method):?array
      {
         return $response??null;
      }
   protected function getLocationID():string|false
   {
      return $this->loadCredentials()['location_id']??false;
@@ -250,4 +288,326 @@
         }
         return $options;
      }
   protected function setContentTypes(): void
   {
      $this->contentTypes = [
         'menu_item' => $this->buildMenuItemContentType(),
         'post'      => $this->buildPostContentType(),
         'event'     => $this->buildEventContentType(),
         'offer'     => $this->buildOfferContentType(),
      ];
   }
      protected function buildMenuItemContentType():array
      {
         return [
            'nutritionFacts'  => [
               'type'   => 'repeater',
               'label'  => 'Nutrition',
               'fields' => [
                  'name'   => [
                     'type'   => 'select',
                     'label'  => 'Name',
                     'options'   => [
                        'calories' => 'Calories',
                        'totalFat'  => 'Total Fat',
                        'cholesterol' => 'Cholesterol',
                        'sodium' => 'Sodium',
                        'totalCarbohydrate' => 'Total Carbohydrates',
                        'protein'   => 'Protein'
                     ]
                  ],
                  'amount' => [
                     'type'   => 'number',
                     'label'  => 'Amount',
                  ],
                  'unit'   => [
                     'type' => 'select',
                     'label'  => 'Unit',
                     'options'   => [
                        'MILLIGRAM' => 'Milligram',
                        'GRAM'   => 'Gram',
                        'CALORIE'   => 'Calorie'
                     ]
                  ]
               ],
               'description'  => 'Recommended. Example: Total Fat: 3g',
               'section'   => 'gmb',
            ],
            'servesNumPeople' => [
               'type'   => 'number',
               'label'  => 'Number of People this Serves',
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'portionSize'  => [
               'type'   => 'text',
               'label'  => 'Portion size',
               'description' => 'Example: 8-piece of nuggets',
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'preparationMethods' => [
               'type'   => 'textarea',
               'label'  => 'Preparation Methods',
               'description'  => 'Specific methods that the food can be prepared in',
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'cuisines'  => [
               'type'   => 'text',
               'label'  => 'Cuisines',
               'description'  => 'Recommended. The specific cuisine of the food item',
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'spiciness' => [
               'type'   => 'select',
               'label'  => 'Spiciness',
               'options'   => [
                  'none'   => 'None',
                  'mild'   => 'Mild',
                  'medium' => 'Medium',
                  'hot' => 'Hot',
               ],
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'allergen'  => [
               'type'   => 'checkbox',
               'label'  => 'Allergens',
               'options'   => [
                  'dairy'  => 'Dairy',
                  'egg' => 'Egg',
                  'fish'   => 'Fish',
                  'peanut' => 'Peanut',
                  'shellfish' => 'Shellfish',
                  'soy'    => 'Soy',
                  'tree nut'  => 'Tree Nut',
                  'wheat'     => 'Wheat'
               ],
               'description'  => 'Recommended',
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ],
            'dietaryRestriction' => [
               'type'   => 'checkbox',
               'label'  => 'Dietary Restrictions',
               'description' => 'Recommended.',
               'options'   => [
                  'halal' => 'Halal',
                  'kosher' => 'Kosher',
                  'organic'   => 'Organic',
                  'vegan'     => 'Vegan',
                  'vegetarian'   => 'Vegetarian',
                  'gluten-free'  => 'Gluten Free'
               ],
               'bulkEdit'  => true,
               'section'   => 'gmb',
            ]
         ];
      }
      protected function buildPostContentType():array
      {
         return [
            'summary' => [
               'type' => 'textarea',
               'label' => 'Post Summary',
               'required' => true
            ],
            'language_code' => [
               'type' => 'text',
               'label' => 'Language',
               'default' => 'en-CA'
            ],
            'media' => [
               'type' => 'image',
               'label' => 'Photo',
            ],
            'call_to_action' => [
               'type' => 'group',
               'fields' => [
                  'type' => [
                     'type' => 'select',
                     'options' =>
                        [
                           'BOOK' => 'Book Now',
                           'ORDER' => 'Order Now',
                           'SHOP' => 'Shop Now',
                           'LEARN_MORE' => 'Learn More',
                           'SIGN_UP' => 'Sign up',
                           'CALL' => 'Call'
                        ]
                  ],
                  'url' => [
                     'type' => 'url',
                     'label' => 'Action URL'
                  ]
               ]
            ]
         ];
      }
      protected function buildEventContentType():array
      {
         return [
            'summary' => [
               'type' => 'textarea',
               'label' => 'Event Description',
               'required' => true
            ],
            'event' => [
               'title' => [
                  'type' => 'text',
                  'label' => 'Event Title',
                  'required' => true
               ],
               'start_date' => [
                  'type' => 'date',
                  'label' => 'Start Date',
                  'required' => true
               ],
               'end_date' => [
                  'type' => 'date',
                  'label' => 'End Date'
               ],
               'start_time' => [
                  'type' => 'time',
                  'label' => 'Start Time'
               ],
               'end_time' => [
                  'type' => 'time',
                  'label' => 'End Time'
               ]
            ],
            'media' => [
               'type' => 'image',
               'label' => 'Event Photo'
            ]
         ];
      }
      protected function buildOfferContentType():array
      {
         return [
            'summary' => [
               'type' => 'textarea',
               'label' => 'Offer Description',
               'required' => true
            ],
            'offer' => [
               'coupon_code' => [
                  'type' => 'text',
                  'label' => 'Coupon Code'
               ],
               'redeem_online_url' => [
                  'type' => 'url',
                  'label' => 'Redemption URL'
               ],
               'terms_conditions' => [
                  'type' => 'textarea',
                  'label' => 'Terms & Conditions']
            ],
            'media' => ['type' => 'image', 'label' => 'Offer Photo']
         ];
      }
   protected function formatForService(int $itemID, string $type = 'post'): array
   {
      // TODO: Implement formatForService() method.
   }
   public function getReviews(int $number = 5):array
   {
      if (!$this->getLocationID() || !$this->getAccountID()){
         return [];
      }
      return $this->cache->remember(
         ['reviews', $this->getLocationID(), $number],
         function() use ($number) {
            try {
               return $this->getRequest(
                  sprintf(
                     "%s/{$this->getAccountID()}/{$this->getLocationID()}/reviews",
                     $this->businessV4
                  ),
                  [
                     'orderBy'   => 'updateTime desc'
                  ],
               );
            } catch (Exception $e) {
               $this->logError(
                  'getReviews',
               'Failed to fetch Reviews',
               [
                  'error'  => $e->getMessage()
               ]);
               return [];
            }
         }
      );
   }
      protected function handleGetReviewResponse(WP_Error|array $response, string $method):array
      {
         return $response;
      }
      /**********************************************************************
       * REVIEW UTILITY
      **********************************************************************/
      /**
       * 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
      {
         $location = $this->getLocation();
         if (!$location) {
            return null;
         }
         if (!empty($location['metadata']['mapsUrl']??'')) {
            return $location['metadata']['mapsUrl'];
         }
         if (!empty($location['metadata']['placeId']??'')) {
            return sprintf(
               'https://search.google.com/local/reviews?placeid=%s',
               urlencode($location['metadata']['placeId'])
            );
         }
         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
      {
         $location = $this->getLocation();
         if (!$location) {
            return null;
         }
         if (!empty($location['metadata']['placeId']??'')) {
            return sprintf(
               'https://search.google.com/local/writereview?placeid=%s',
               urlencode($location['metadata']['placeId'])
            );
         }
         if (!empty($location['metadata']['mapsUrl']??'')) {
            return sprintf(
               '%s/reviews',
               $location['metadata']['mapsUrl']
            );
         }
         return null;
      }
   /**************************************************************************
    * FOOD MENU HELPERS
   **************************************************************************/
}