Jake Vanderwerf
5 days ago 0dfe1d8afafc59c4a5559c498342668d5a58d6ef
inc/integrations/services/Square.php
@@ -2,177 +2,100 @@
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Admin;
use JVBase\integrations\APIEndpoint;
use JVBase\integrations\Instance;
use JVBase\integrations\Integrations;
use JVBase\integrations\OAuth;
use JVBase\integrations\OAuthCredentials;
use JVBase\integrations\OAuthURLs;
use JVBase\integrations\Requests;
use JVBase\integrations\SyncDefaults;
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\Webhooks;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Form;
use JVBase\meta\Meta;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
use JVBase\ui\Checkout;
use JVBase\ui\CRUDSkeleton;
use WP_Error;
use WP_Post;
use WP_Query;
use WP_User;
if (!defined('ABSPATH')) {
   exit;
}
/**
 * Square Integration Class
 *
 * Handles OAuth 2.0 authentication and API interactions with Square
 * Uses the latest Square API version with OAuth code flow
 *
 * @since 1.0.0
 */
class Square extends Integrations
{
   protected static string $syncCustomer = 'square_sync_customer';
   protected array $allowedContent = [
      'REGULAR',
      'FOOD_AND_BEV',
      'APPOINTMENTS_SERVICE',
      'DIGITAL',
      'EVENT',
      'DONATION'
   ];
   /**
    * Square API Configuration
    */
   protected string $service_name = 'square';
   protected array|string $apiBase = [
      'production'   => 'https://connect.squareup.com',
      'sandbox'      => 'https://connect.squareupsandbox.com'
   ];
   protected string $apiVersion = '2025-09-24';
   protected string $environment = 'sandbox';
   private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins, for customers
   /**
    * OAuth Configuration
    */
   protected bool $isOAuthService = true;
   use Admin,
      Instance,
      Requests,
      OAuth,
      SyncFrom,
      SyncTo,
      SyncDefaults,
      Webhooks;
   protected string $orderPostType = '_square_order';
   protected array $newOrder = [];
   protected array $oauth = [
      'authorize' => '',
      'token' => '',
      'revoke' => '',
      'scopes' => [
         'MERCHANT_PROFILE_READ',
         'MERCHANT_PROFILE_WRITE',
         'PAYMENTS_WRITE',
         'PAYMENTS_READ',
         'CUSTOMERS_WRITE',
         'CUSTOMERS_READ',
         'INVENTORY_READ',
         'INVENTORY_WRITE',
         'ITEMS_READ',
         'ITEMS_WRITE',
         'ORDERS_READ',
         'ORDERS_WRITE'
      ]
   protected string $apiVersion = '2026-07-15';
   public bool $hasBatchCreate = true;
   public bool $hasBatchUpdate = true;
   public bool $hasBatchDelete = true;
   public array $apiEndpoints = [
   ];
   /**
    * Square-specific properties
    */
   protected string $merchantId = '';
   protected string $locationId = '';
   protected array $locations = [];
   protected static Square $instance;
   public static function getInstance():self
   protected function __construct()
   {
      if (!isset(self::$instance)) {
         self::$instance = new self();
      }
      return self::$instance;
   }
   protected function __construct(?int $userID = null)
   {
      // Display properties
      $this->title = 'Square';
      $this->icon = 'square-logo';
      $this->supportsWebp = false;
      $this->refresh_interval = 7 * DAY_IN_SECONDS;
      $this->newOrder = [
         'post_type'    => $this->orderPostType,
         'post_status'  => 'PROPOSED',
      $this->canSync = [
         'create' => true,
         'update' => true,
         'delete' => true,
      ];
      // Define credential fields
      $this->fields = [
         'environment'  => [
            'type'   => 'select',
            'label'  => 'Environment',
            'options'   => [
               'sandbox'   => 'Sandbox',
               'production'=> 'Production',
            ],
            'default'   => 'sandbox',
         ],
         'client_id' => [
            'type' => 'text',
            'label' => 'Application ID',
            'hint' => 'Found in Square Developer Dashboard',
            'required' => true,
         ],
         'client_secret' => [
            'type' => 'text',
            'subtype' => 'password',
            'label' => 'Application Secret',
            'hint' => 'Found in OAuth section of Square Developer Dashboard',
            'required' => true,
         ],
         'location_id' => [
            'type' => 'select',
            'label' => 'Default Location',
            'options' => [],
            'hint' => 'Select default Square location for transactions',
         ]
      ];
      $this->defineFields();
      $this->defineInstructions();
      $this->defineOAuth();
      // Advanced settings
      $this->advanced = [
         'webhook_signature_key' => [
            'type' => 'text',
            'subtype' => 'password',
            'label' => 'Webhook Signature Key',
            'hint' => 'Used to verify webhook authenticity',
         ],
         'access_token' => [
            'type' => 'text',
            'subtype' => 'password',
            'label' => 'Access Token',
            'hint' => 'Generated automatically after OAuth authorization',
            'readonly' => true,
         ],
         'refresh_token' => [
            'type' => 'text',
            'subtype' => 'password',
            'label' => 'Refresh Token',
            'hint' => 'Used to refresh expired access tokens',
            'readonly' => true,
         ],
         'merchant_id' => [
            'type' => 'text',
            'label' => 'Merchant ID',
            'hint' => 'Square Merchant ID (obtained after authorization)',
            'readonly' => true,
         ],
      ];
      $this->baseUrl = $this->getBaseURL();
      parent::__construct();
      $this->defineActions();
   }
   protected function defineFields():void
   {
      $env = $this->addField('environment', 'Environment','select','manage_options');
         $env->setOptions([
            'sandbox'   => 'Sandbox',
            'production'=> 'Production'
         ]);
         $env->setDefault('sandbox');
      $id = $this->addField('client_id', 'Application ID', 'text', 'manage_options');
         $id->setRequired();
         $id->setHint('Found in Square Developer Dashboard');
      $sec = $this->addField('client_secret', 'Application Secret', 'text', 'manage_options');
         $sec->setSubType('password');
         $sec->setRequired();
         $sec->setHint('Found in OAuth section of Square Developer Dashboard');
      //Locations are set from the first response from the integration
      $locs = $this->addField('locations', 'Locations', 'select','manage_options');
         $locs->setHidden();
         $locs->setOptions($this->getLocationOptions());
      $loc = $this->addField('location_id', 'Default Location', 'select', 'manage_options');
         $loc->setOptions($this->loadCredentials()['locations']);
         $loc->setHint('Select default Square location for transactions');
   }
   protected function defineInstructions():void
   {
      $this->instructions = [
         'Go to <a href="https://developer.squareup.com/apps" target="_blank">Square Developer Dashboard</a>',
         'Create a new application or select an existing one',
@@ -181,79 +104,656 @@
         'Copy the Application ID and Application Secret',
         'Click "Authorize Connection" below to complete OAuth flow'
      ];
   }
      $this->canSync = [
         'create' => true,
         'update' => true,
         'delete' => true
      ];
      $this->supportsWebp = false;
      $this->hasWebhooks = true;
      parent::__construct();
      // Add Square-specific actions
      $this->actions = array_merge(
         $this->actions,
         [
            'select_location' => 'handleSelectLocation',
            'fetch_locations' => 'handleFetchLocations',
            'verify_webhook' => 'handleVerifyWebhook',
            'test_payment' => 'handleTestPayment'
         ]
   protected function defineActions():void
   {
      $this->addAction(
         'Select Location',
         [$this, 'handleSelectLocation'],
         'select_location',
         'building-office',
      );
      $this->buttons = array_merge(
         $this->buttons,
         [
            'import_from_square' => 'Import Catalog from Square',
            'sync_to_square' => 'Sync Site to Square',
         ]
      $this->addAction(
         'Fetch Locations',
         [$this, 'getLocations'],
         'fetch_locations',
         'building-office',
      );
      $this->addAction(
         'Import From Square',
         [$this, 'handleImportFromSquare'],
         'import_from_square',
         'download-simple'
      );
      $this->addAction(
         'Sync to Square',
         [$this, 'handleSyncToSquare'],
         'sync_to_square',
         'export',
      );
//    $this->addAction(
//       'Verify Webhook',
//       [$this, 'handleVerifyWebhook'],
//       'verify_webhook',
//    );
   }
   protected function defineOAuth():void
   {
      $this->baseOAuthUrl = $this->getBaseURL();
      $base = $this->getBaseURL();
      $this->oauth = new OAuthURLs(
         sprintf('%s/oauth2/authorize', $base),
         sprintf('%s/oauth2/token', $base),
         sprintf('%s/oauth2/revoke', $base)
      );
   }
   protected function registerAdditionalHooks():void
   {
      add_action('init', [$this, 'registerSquarePostTypes'], 5);
      add_action('init', [$this, 'addDashboardPages'], 10);
      add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']);
   }
   /**
    * Initialize the integration with loaded credentials
    */
   protected function initialize(): void
   {
      if (empty($this->credentials)) {
         $this->loadCredentials();
      $this->defineEndpoints();
   }
      protected function defineEndpoints():void
      {
         $this->defineCustomerEndpoints();
         $this->defineProductEndpoints();
         $this->defineOrderEndpoints();
         $this->definePaymentEndpoints();
      }
         protected function defineCustomerEndpoints():void
         {
            $customer = new APIEndpoint();
            $customer->setCreate('/v2/customers');
            $customer->setBatchCreate('/v2/customers/bulk-create');
            $customer->setDelete('/v2/customers/');//add the item_id to the end
            $customer->setBatchDelete('/v2/customers/bulk-delete');
            $customer->setUpdate('/v2/customers/');//add the item_id to the end
            $customer->setBatchUpdate('/v2/customers/bulk-update');
            $customer->setSearch('/v2/customers/search');
            $customer->setImport('/v2/customers');
            $this->apiEndpoints['customer'] = $customer;
         }
         protected function defineProductEndpoints():void
         {
            $product = new APIEndpoint();
            $product->setSearch('/v2/catalog/search-catalog-items');
            $product->setImport('/v2/catalog/list');
            $product->setImage('/v2/catalog/images');
            $product->setCreate('/v2/catalog/object');
            $product->setBatchCreate('/v2/catalog/batch-upsert');
            $product->setUpdate('/v2/catalog/object');
            $product->setBatchUpdate('/v2/catalog/batch-upsert');
            $product->setDelete('/v2/catalog/object/');//add the item_id to the end
            $product->setBatchDelete('/v2/catalog/batch-delete');
            $this->apiEndpoints['product'] = $product;
         }
         protected function defineOrderEndpoints():void
         {
            $order = new APIEndpoint();
            $order->setCreate('/v2/orders');
            $order->setSearch('/v2/orders/search');
            $order->setUpdate('/v2/orders/'); //add order_id
            $this->apiEndpoints['order'] = $order;
         }
         protected function definePaymentEndpoints():void
         {
            $payment = new APIEndpoint();
            $payment->setCreate('/v2/payments');
            $this->apiEndpoints['payment'] = $payment;
         }
   protected function refreshAccessToken(OAuthCredentials $credentials): array
   {
      if (!$this->getClientID() || !$this->getClientSecret())
      $response = wp_remote_post(
         $this->oauth->getToken(),
         [
            'body'   => [
               'client_id'    => $this->getClientID(),
               'client_secret' => $this->getClientSecret(),
               'refresh_token'   => $credentials->refresh_token,
               'grant_type'   => 'refresh_token'
            ],
            'headers'   => $this->getOAuthRequestHeaders()
         ]
      );
      if (is_wp_error($response)) {
         $this->logError(
            'refreshAccessToken',
         'Failed to refresh Square token',
            [
               'error'  => $response->get_error_message()
            ]
         );
         return $this->response(false, 'Could not refresh token');
      }
      $this->merchantId = $this->credentials['merchant_id'] ?? '';
      $this->locationId = $this->credentials['location_id'] ?? '';
      $this->environment = $this->credentials['environment'] ?? 'sandbox';
      $data = json_decode(wp_remote_retrieve_body($response));
      if (isset($data['access_token'])) {
         $credentials = [
            'access_token' =>  $data['access_token'],
            'expires_at'   => time() + ($data['expires_in'] ?? 2592000) // 30 days default
         ];
      // Set OAuth URLs based on environment
      $baseUrl = ($this->environment === 'production')
         ? 'https://connect.squareup.com'
         : 'https://connect.squareupsandbox.com';
      $this->oauth['authorize'] = $baseUrl . '/oauth2/authorize';
      $this->oauth['token'] = $baseUrl . '/oauth2/token';
      $this->oauth['revoke'] = $baseUrl . '/oauth2/revoke';
      // Set up API endpoints with version
      $this->apiEndpoints = [
         'locations' => '/v2/locations',
         'customers' => '/v2/customers',
         'catalog' => '/v2/catalog',
         'payments' => '/v2/payments',
         'orders' => '/v2/orders',
         'inventory' => '/v2/inventory'
      ];
         $this->saveCredentials($credentials);
         return $this->response(true, 'Successfully refreshed access token');
      }
      return $this->response(false, 'Something went wrong that wasn\'t caught - could not refresh access token');
   }
   public function getOrderFields():array
   protected function getRequestHeaders(): array
   {
      return [
         'Authorization' => 'Bearer ' . ($this->getAccessToken()),
         'Square-Version' => $this->apiVersion,
         'Content-Type' => 'application/json'
      ];
   }
   protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
   {
      if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
         return $this->response(false, 'Rate limit exceeded');
      }
      $endpoint = str_replace($this->getBaseURL(), '', $endpoint);
      switch ($endpoint) {
         case '/v2/customers/search':
            return $this->handleEmailSearchResponse($response, $method);
         case '/v2/customers':
            return $this->handleCreateUserResponse($response, $method);
         case '/v2/locations':
            return $this->handleGetLocationResponse($response, $method);
      }
      return $this->response(false, 'Unhandled Response for '.$endpoint.' endpoint, with method: '.$method);
   }
   protected function setContentTypes(): void
   {
      $base = $this->getBaseFields();
      $this->contentTypes = [
         'customer'           => $this->getCustomerFields(),
         'REGULAR'            => $base,
         'FOOD_AND_BEV'       => array_merge($base, $this->setFoodAndBevFields()),
         'APPOINTMENTS_SERVICE'  => array_merge($base, $this->setAppointmentServiceFields()),
         'EVENT'              => array_merge($base, $this->setEventFields()),
         'GIFT_CARD'          => array_merge($base, $this->setGiftCardFields()),
      ];
   }
      protected function getCustomerFields():array
      {
         return [
            'address'   => [
               'type'   => 'group',
               'fields' => [
                  'address_line_1'  => [
                     'type'   => 'text',
                     'label'  => 'Address Line 1',
                     'hint'   => 'ex: 6551 111 St NW',
                     'required'  => true,
                     'section'   => 'address'
                  ],
                  'address_line_2'  => [
                     'type'   => 'text',
                     'label'  => 'Address Line 2',
                     'hint'   => 'ex: Unit 2',
                     'section'   => 'address'
                  ],
                  'locality'  => [
                     'type'   => 'text',
                     'label'=> 'City',
                     'section'   => 'address',
                     'required'  => true,
                  ],
                  'administrative_district_level_1' => [
                     'type'   => 'text',
                     'label' => 'Province',
                     'hint' => 'The two-character code, example: AB',
                     'default'=> 'AB',
                     'section'   => 'address',
                     'required'  => true,
                  ],
                  'postal_code' => [
                     'type'   => 'text',
                     'label'  => 'Postal Code',
                     'section'=> 'address'
                  ],
                  'country'   => [
                     'type'   => 'text',
                     'label'  => 'Country Code',
                     'hint'   => 'The tw-character country code, example: CA',
                     'default'   => 'CA',
                     'section'   => 'address',
                     'required'  => true,
                  ],
                  'first_name'   => [
                     'type'   => 'text',
                     'label'  => 'First Name (if different)',
                     'hint'   => 'Optional field to save the address to someone that isn\'t you'
                  ],
                  'last_name' => [
                     'type'   => 'text',
                     'label'  => 'Last Name (if different)',
                     'hint'   => 'Optional field to save the address to someone that isn\'t you'
                  ]
               ]
            ]
         ];
      }
      protected function getBaseFields():array
      {
         return [
            'price' => [
               'type'        => 'number',
               'label'       => 'Price',
               'step'        => 0.01,
               'max'         => 99999,
               'description' => 'Price for this variation'
            ],
            'sku' => [
               'type'        => 'text',
               'label'       => 'SKU',
               'description' => 'Stock keeping unit'
            ],
            'cart_quantity'   => [
               'type'   => 'number',
               'label'  => 'Quantity',
               'hidden' => true,
            ],
            'available_for_pickup' => [
               'type'   => 'true_false',
               'label'  => 'Available for Pick Up',
               'section'=> 'square-advanced'
            ],
            'available_online' => [
               'type'   => 'true_false',
               'label'  => 'Available online',
               'section'=> 'square-advanced'
            ],
            'product_variations' => [
               'type'        => 'repeater',
               'label'       => 'Product Variations',
               'description' => 'Different versions of this product (sizes, colors, etc.)',
               'add_label'   => 'Add Variation',
               'section'     => 'variations',
               'fields'      => $this->setVariationFields()
            ],
         ];
      }
         protected function setVariationFields(?string $type = null):array
         {
            $fields = [
               'name' => [
                  'type'        => 'text',
                  'label'       => 'Variation Name',
                  'description' => 'e.g., "Small", "Large", "Red", etc.'
               ],
               'sku' => [
                  'type'        => 'text',
                  'label'       => 'SKU',
                  'description' => 'Stock keeping unit'
               ],
               'price' => [
                  'type'        => 'number',
                  'label'       => 'Price',
                  'step'        => 0.01,
                  'max'         => 99999,
                  'description' => 'Price for this variation'
               ],
               'track_inventory' => [
                  'type'   => 'true_false',
                  'label'  => 'Track Inventory',
               ],
               'item_id' => [
                  'type'        => 'text',
                  'label'       => 'Square Variation ID',
                  'description' => 'Square catalog ID for this variation',
                  'hidden'      => true
               ],
               'last_sync' => [
                  'type'   => 'datetime',
                  'label'  => 'Last Sync',
                  'hidden' => true
               ]
            ];
            switch ($type) {
               case 'FOOD_AND_BEV':
                  $fields['ingredients']  = [
                     'type'   => 'textarea',
                     'label'  => 'Ingredients List',
                     'description'  => 'Separate ingredients with commas',
                  ];
                  $fields['preparation_time_duration']   = [
                     'type'   => 'number',
                     'label'  => 'Preparation time (in minutes)',
                  ];
                  break;
               case 'GIFT_CARD':
                  $fields['gift_card_type']  = [
                     'type'   => 'select',
                     'label'  => 'Gift Card Type',
                     'options'   => [
                        'PHYSICAL'  => 'Physical',
                        'DIGITAL'   => 'Digital',
                     ],
                     'default'   => 'DIGITAL',
                  ];
                  break;
               case 'APPOINTMENTS_SERVICE':
                  $fields['service_duration']   = [
                     'type'   => 'number',
                     'label'  => 'Duration of Service in Minutes'
                  ];
                  $fields['available_for_booking'] = [
                     'type'   => 'true_false',
                     'label'  => 'Available for Booking'
                  ];
                  break;
            }
            return array_combine(
               array_map(fn($k) => '_square_' . $k, array_keys($fields)),
               $fields
            );
         }
         protected function setFoodAndBevFields():array
         {
            return [
               'ingredients'  => [
                  'type'   => 'textarea',
                  'label'  => 'Ingredients',
                  'hint'   => 'A comma separated list of ingredients'
               ],
               'preparation_time_duration' => [
                  'label'  => 'Preparation Time',
                  'type'   => 'number',
                  'hint'   => 'Preparation time in minutes'
               ],
               'product_variations' => [
                  'type'        => 'repeater',
                  'label'       => 'Product Variations',
                  'description' => 'Different versions of this product (sizes, colors, etc.)',
                  'add_label'   => 'Add Variation',
                  'section'     => 'variations',
                  'fields'      => $this->setVariationFields('FOOD_AND_BEV')
               ],
            ];
            /* TODO: Set definitions for:
               'dietary_preferences'   => [
               ],
               'calories_text'   => [
               ],
            */
         }
         protected function setAppointmentServiceFields():array
         {
            return [
               'product_variations' => [
                  'type'        => 'repeater',
                  'label'       => 'Product Variations',
                  'description' => 'Different versions of this product (sizes, colors, etc.)',
                  'add_label'   => 'Add Variation',
                  'section'     => 'variations',
                  'fields'      => $this->setVariationFields('APPOINTMENTS_SERVICE')
               ],
            ];
         }
         protected function setEventFields():array
         {
            return [
               'venue_details'   => [
                  'type'   => 'textarea',
                  'label'  => 'Venue Details',
               ],
               'capacity'  => [
                  'type'   => 'number',
                  'label'  => 'Capacity'
               ],
               'product_variations' => [
                  'type'        => 'repeater',
                  'label'       => 'Product Variations',
                  'description' => 'Different versions of this product (sizes, colors, etc.)',
                  'add_label'   => 'Add Variation',
                  'section'     => 'variations',
                  'fields'      => $this->setVariationFields('EVENT')
               ],
            ];
         }
         protected function setGiftCardFields():array
         {
            return [
      //       'allowed_locations'  => [
      //
      //       ],
               'product_variations' => [
                  'type'        => 'repeater',
                  'label'       => 'Product Variations',
                  'description' => 'Different versions of this product (sizes, colors, etc.)',
                  'add_label'   => 'Add Variation',
                  'section'     => 'variations',
                  'fields'      => $this->setVariationFields('GIFT_CARD')
               ],
            ];
         }
   protected function formatForService(int $itemID, string $type = 'post'): array
   {
      return match ($type) {
         'post'   => $this->formatPostItem($itemID),
         'user'   => $this->formatUserItem($itemID),
         'term'   => $this->formatTermItem($itemID),
         default => []
      };
   }
   public function handleEmailSearch(string $email): string|false
   {
      try {
         $response = $this->postRequest(
            sprintf('%s/v2/customers/search', $this->getBaseURL()),
            [
               'query'  => [
                  'filter' => [
                     'email_address'   => [
                        'exact'  => $email
                     ]
                  ]
               ]
            ]
         );
         return $response['data'];
      } catch (Exception $e) {
         return false;
      }
   }
      protected function handleEmailSearchResponse(WP_Error|array $response, string $method):array
      {
         $data = empty($response['customers'])
            ? false
            : $response['customers'][0]??false;
         return $this->response(true, 'Successfully got results.', $data);
      }
   protected function handleCreateUser(array $data): array
   {
      try {
         return $this->postRequest(
            sprintf(
               '%s%s',
               $this->getBaseURL(),
               $this->apiEndpoints['customer']['create']->getEndpoint()
            ),
            $data
         );
      } catch (Exception $e) {
         return $this->response(false, 'Failed to create customer: '.$e->getMessage());
      }
   }
      protected function handleCreateUserResponse(WP_Error|array $response, string $method):array
      {
         $data = empty($response['customer'])
            ? false
            : $response['customer'];
         return $this->response(true, 'Successfully got results.', $data);
      }
   protected function validateUserFields(int $userID, array $fields):array|false
   {
      $allowed = [
         'given_name',
         'family_name',
         'company_name',
         'email_address',
         'phone_number',
         'idempotency_key',
         'nickname',
         'address',
         'reference_id', // The WP user id, in this case
         'note',
         'birthday'
      ];
      $fields = array_filter($fields, function ($f) use ($allowed, $fields) {
         if (!in_array($f, $allowed)) {
            $this->logError('validateUserFields', 'Stripped attempted field for user: '.$f.' with value: '.print_r($fields[$f], true));
         }
         return in_array($f, $allowed) && !empty($fields[$f]);
      }, ARRAY_FILTER_USE_KEY);
      if (empty($fields)) {
         $this->logError('validateUserFields', 'No valid fields found');
         return false;
      }
      //Square requires at least one of these fields
      $any = [
         'given_name',
         'family_name',
         'company_name',
         'email_address',
         'phone_number'
      ];
      $doIt = false;
      foreach ($any as $a) {
         if (array_key_exists($a, $fields) && !empty($fields[$a])) {
            $doIt = true;
         }
      }
      if (!$doIt) {
         $this->logError('validateUserFields', 'We need at least one of the following fields: '.print_r($any, true));
         return false;
      }
      $addressAllowed = [
         'address_line_1',
         'address_line_2',
         'address_line_3',
         'locality',       // city
         'sublocality',    //
         'sublocality_2',  //
         'sublocality_3',  //
         'administrative_district_1',  //Province, in our case
         'administrative_district_2',  //
         'administrative_district_3',  //
         'postal_code',
         'country',
         'first_name',
         'last_name',
      ];
      if (array_key_exists('address', $fields)) {
         $fields['address'] = array_filter($fields['address'], function ($f) use ($addressAllowed, $fields){
            return array_key_exists($f, $addressAllowed) && !empty($fields['address'][$f]);
         }, ARRAY_FILTER_USE_KEY);
      }
      foreach ($fields as $f => $v) {
         $v = match ($f) {
            'email' => filter_var($v, FILTER_SANITIZE_EMAIL),
            'phone' => Sanitizer::sanitizePhone($v),
            default => sanitize_text_field($v),
         };
         if (empty($v) && in_array($f, $this->requiredUserFields())) {
            return false;
         }
         $fields[$f] = $v;
      }
      //Add the WP User ID as the reference
      $fields['reference_id'] = $userID;
      return $fields;
   }
   protected function validateWebhook(array $payload): bool
   {
      // TODO: Implement validateWebhook() method.
   }
   protected function processWebhook(array $payload): bool
   {
      // TODO: Implement processWebhook() method.
   }
   protected function extractWebhookId(array $payload): string
   {
      // TODO: Implement extractWebhookId() method.
   }
   /********************************************************************
    * CUSTOM POST TYPES
   ********************************************************************/
   protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
   {
      $queue = JVB()->queue();
      $queue->registry()->register(
         self::$syncCustomer,
         new TypeConfig(
            executor: $executor,
            maxRetries: 1,
         )
      );
      $queue->registry()->register(
         self::$import,
         new TypeConfig(
            executor: $executor,
            maxRetries: 2
         )
      );
   }
   public function registerSquarePostTypes():void
      {
         $orders = Registrar::forPost($this->orderPostType, 'Square Order', 'Square Orders');
            $orders->make([
               'public' => true
            ]);
            $orders->setAll(['system']);
         $fields = $orders->fields();
         foreach ($this->getOrderFields() as $fieldName => $config) {
            $fields->addField($fieldName, $config);
         }
      }
         public function getOrderFields():array
         {
            return [
               'post_title' => [
                  'type' => 'text',
                  'label' => 'Order Number'
@@ -341,3152 +841,125 @@
                  'label' => 'Last Updated',
               ]
            ];
   }
   public function registerSquarePostTypes():void
   {
      $orders = Registrar::forPost($this->orderPostType, 'Square Order', 'Square Orders');
      $orders->make([
         'public' => true
      ]);
      $orders->setAll(['system']);
      $fields = $orders->fields();
      foreach ($this->getOrderFields() as $fieldName => $config) {
         $fields->addField($fieldName, $config);
      }
   }
   /**
    * Get request headers for API calls
    */
   protected function getRequestHeaders(): array
   {
      return [
         'Authorization' => 'Bearer ' . ($this->credentials['access_token'] ?? ''),
         'Square-Version' => $this->apiVersion,
         'Content-Type' => 'application/json'
      ];
   }
   /**
    * Add Square-specific OAuth parameters
    */
   protected function addOAuthParams(array $params): array
   {
      // Load current environment setting
      $this->ensureInitialized();
      // For production, add session=false
      // For sandbox, session parameter is not supported and should be omitted
      if ($this->environment === 'production') {
         $params['session'] = 'false';
      } else {
         // Ensure session is not included for sandbox
         unset($params['session']);
      }
      return $params;
   }
   /**
    * Exchange OAuth code for tokens
    * Override to handle Square's specific response format
    */
   protected function exchangeOAuthCode(string $code): ?array
   {
      $this->ensureInitialized();
      // Prepare the request body as an array
      $body = [
         'client_id' => $this->credentials['client_id'] ?? '',
         'client_secret' => $this->credentials['client_secret'] ?? '',
         'code' => $code,
         'grant_type' => 'authorization_code',
         'redirect_uri' => $this->getRedirectUri()
      ];
      $response = wp_remote_post($this->oauth['token'], [
         'body' => json_encode($body),
         'headers' => [
            'Content-Type' => 'application/json',
            'Square-Version' => $this->apiVersion
         ]
      ]);
      if (is_wp_error($response)) {
         $this->logError('OAuth token exchange failed', ['error' => $response->get_error_message()]);
         return null;
      }
      $data = json_decode(wp_remote_retrieve_body($response), true);
      if (isset($data['access_token'])) {
         return [
            'access_token' => $data['access_token'],
            'refresh_token' => $data['refresh_token'] ?? '',
            'expires_in' => $data['expires_in'] ?? 2592000, // 30 days default
            'token_type' => $data['token_type'] ?? 'Bearer',
            'merchant_id' => $data['merchant_id'] ?? '',
            'scope' => $data['scope'] ?? ''
         ];
      }
      $this->logError('Failed to obtain access token', ['response' => $data]);
      return null;
   }
   /**
    * Add Square-specific credential data after OAuth
    */
   protected function addCredentialData(array $credentials, array $tokens): array
   {
      // Store Square-specific data
      $credentials['merchant_id'] = $tokens['merchant_id'] ?? '';
      // Preserve selected location if it exists
      $credentials['location_id'] = $this->credentials['location_id'] ?? '';
      // Preserve sandbox setting
      $credentials['environment'] = $this->credentials['environment'] ?? 'sandbox';
      return $credentials;
   }
   /**
    * Refresh OAuth token
    * Override to handle Square's refresh token flow
    */
   protected function refreshOAuthToken(): bool
   {
      if (!$this->isOAuthService || empty($this->credentials['refresh_token'])) {
         return false;
      }
      $response = wp_remote_post($this->oauth['token'], [
         'body' => [
            'client_id' => $this->credentials['client_id'],
            'client_secret' => $this->credentials['client_secret'],
            'refresh_token' => $this->credentials['refresh_token'],
            'grant_type' => 'refresh_token'
         ],
         'headers' => $this->getRequestHeaders()
      ]);
      if (is_wp_error($response)) {
         $this->logError('Failed to refresh Square token', [
            'error' => $response->get_error_message()
         ]);
         return false;
      }
      $data = json_decode(wp_remote_retrieve_body($response), true);
      if (isset($data['access_token'])) {
         $this->credentials['access_token'] = $data['access_token'];
         $this->credentials['expires_at'] = time() + ($data['expires_in'] ?? 2592000); // 30 days
         // Note: Square returns the SAME refresh token
         if (isset($data['refresh_token'])) {
            $this->credentials['refresh_token'] = $data['refresh_token'];
         }
         $this->saveCredentials($this->credentials);
         return true;
      }
      return false;
   }
   /**
    * Load Square locations
    */
   protected function loadLocations(): void
   /********************************************************************
    * UTILITY METHODS
    ********************************************************************/
   protected function getEnvironment():string|false
   {
      // Skip if we don't have credentials yet (during OAuth flow)
      if (empty($this->credentials['access_token'])) {
         return;
      return $this->loadCredentials()['environment']??false;
   }
   protected function getClientID():string|false
   {
      return $this->loadCredentials()['client_id']??false;
   }
   protected function getClientSecret():string|false
   {
      return $this->loadCredentials()['client_secret']??false;
   }
   protected function getMerchantID():string|false
   {
      return $this->loadCredentials()['merchant_id']??false;
   }
   protected function getLocationID():string|false
   {
      return $this->loadCredentials()['location_id']??false;
   }
   protected function getLocations():array|false
   {
      if (!$this->getAccessToken()) {
         return false;
      }
      try {
         $response = $this->getRequest(
            '/v2/locations',
            [],
            $this->environment
         return $this->getRequest(
            sprintf('%s/v2/locations', $this->getBaseURL()),
         );
      } catch (Exception $e) {
         $this->logError(
            'getLocations',
            'Failed to load locations',
            [
               'error'  => $e->getMessage()
            ]
         );
      }
      return [];
   }
      protected function handleGetLocationResponse(WP_Error|array $response, string $method):array
      {
         if (isset($response['locations'])) {
            $this->locations = $response['locations'];
            $locations = $response['locations'];
            // Update location options in fields
            $locationOptions = [];
            foreach ($this->locations as $location) {
               if ($location['status'] === 'ACTIVE') {
                  $locationOptions[$location['id']] = $location['name'] ?? $location['id'];
            $options=[];
            foreach ($locations as $l) {
               if ($l['status'] === 'ACTIVE') {
                  $options[$l['id']] = $l['name']??$l['id'];
               }
            }
            if (!empty($locationOptions)) {
               $this->fields['location_id']['options'] = $locationOptions;
            if (!empty($options)) {
               $this->saveCredentials(['locations' => $options], false);
            }
            return $this->response(true, 'Found locations. Stored in table for reference.', $locations);
         }
      } catch (Exception $e) {
         $this->logError('Failed to load locations', ['error' => $e->getMessage()]);
         // Set empty locations array so we don't retry constantly
         $this->locations = [];
         return $this->response(false, 'No locations found.');
      }
   }
   protected function renderOAuthConnectedOptions(): void
      protected function getLocationOptions():array
      {
         return $this->loadCredentials()['locations']??[];
      }
   protected function getBaseURL():string
   {
      if (!$this->isSetUp()) {
         return;
      }
      // Check OAuth status
      $oauth_valid = $this->isOAuthValid();
      if (!$oauth_valid) {
         ?>
         <div class="oauth-status">
            <span class="status-invalid">⚠️ OAuth token expired - please reconnect</span>
         </div>
         <?php
      }
      $this->loadLocations();
      if (!empty($this->locations)) {
         ?>
         <div class="form-field" data-service="square">
            <label for="square_location_id">Square Location</label>
            <select name="location_id"
                  id="square_location_id"
                  class="jvb-ajax-update"
                  data-action="select_location">
               <option value="">Select a location...</option>
               <?php foreach ($this->locations as $location): ?>
                  <option value="<?php echo esc_attr($location['id']); ?>"
                     <?php selected($this->credentials['location_id'] ?? '', $location['id']); ?>>
                     <?php echo esc_html($location['name']); ?>
                  </option>
               <?php endforeach; ?>
            </select>
         </div>
         <?php
         // Show current selection status
         if (!empty($this->credentials['location'])) {
            ?>
            <div class="connection-status connected">
               ✅ Connected to: <?php echo esc_html($this->credentials['location_id']); ?>
            </div>
            <?php
         }
      } else {
         ?>
         <div class="notice notice-warning">
            <p>No Square locations found. Please ensure your Square credentials are correct.</p>
         </div>
         <?php
      }
      return $this->getEnvironment() === 'production'
         ? 'https://connect.squareup.com'
         : 'https://connect.squareupsandbox.com';
   }
   /**
    * Handle location selection
    */
   public function handleSelectLocation($data): array
   protected function getAccessToken():string
   {
      if (empty($data['location_id'])) {
         return [
            'success' => false,
            'message' => 'No location selected'
         ];
      }
      $this->credentials['location_id'] = sanitize_text_field($data['location_id']);
      $this->locationId = $this->credentials['location_id'];
      $result = $this->saveCredentials($this->credentials);
      if ($result['success']) {
         return [
            'success' => true,
            'message' => 'Location updated successfully'
         ];
      }
      return $result;
      return $this->loadCredentials()['access_token']??'';
   }
   /**
    * Handle fetching locations
    */
   public function handleFetchLocations(): array
   /***************************************************************
    * ACTIONS
   ***************************************************************/
   public function handleSelectLocation(array $data):array
   {
      try {
         $this->loadLocations();
         if (empty($this->locations)) {
            return [
               'success' => false,
               'message' => 'No locations found'
            ];
         }
         return [
            'success' => true,
            'data' => $this->locations,
            'message' => sprintf('Found %d location(s)', count($this->locations))
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => 'Failed to fetch locations: ' . $e->getMessage()
         ];
      }
   }
   /**
    * Handle webhook verification
    */
   public function handleVerifyWebhook($data): array
   {
      if (empty($data['signature']) || empty($data['body'])) {
         return [
            'success' => false,
            'message' => 'Missing signature or body'
         ];
      if (empty($data['location_id']??'')) {
         return $this->response(false, 'No location selected');
      }
      $signatureKey = $this->credentials['webhook_signature_key'] ?? '';
      if (empty($signatureKey)) {
         return [
            'success' => false,
            'message' => 'Webhook signature key not configured'
         ];
      }
      // Calculate expected signature
      $stringToSign = $data['url'] . $data['body'];
      $expectedSignature = base64_encode(
         hash_hmac('sha256', $stringToSign, $signatureKey, true)
      );
      $isValid = hash_equals($expectedSignature, $data['signature']);
      return [
         'success' => $isValid,
         'message' => $isValid ? 'Valid webhook signature' : 'Invalid webhook signature'
      $credentials = [
         'location_id'  => sanitize_text_field($data['location_id'])
      ];
      return $this->saveCredentials($credentials, false);
   }
   /**
    * Test payment/connection
    */
   public function handleTestPayment(): array
   public function handleFetchLocations():array
   {
      try {
         // Test with a simple API call to verify connection
         $response = $this->getRequest(
            '/v2/locations/'.$this->locationId,
            [],
            $this->environment
         $locations = $this->getLocations();
         if (!$locations) {
            return $this->response(false, 'No locations found');
         }
         return $this->response(
            true,
            sprintf('Found %d location(s)', count($locations)),
            $locations
         );
         if (!empty($response['location'])) {
            return [
               'success' => true,
               'message' => 'Successfully connected to Square',
               'data' => [
                  'location_name' => $response['location']['name'] ?? 'Unknown',
                  'merchant_id' => $this->merchantId,
                  'environment' => $this->environment
               ]
            ];
         }
         return [
            'success' => false,
            'message' => 'Connection test failed'
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => 'Connection test failed: ' . $e->getMessage()
         ];
         return $this->response(false, 'Failed to fetch locations: '.$e->getMessage());
      }
   }
   /**
    * Create a Square customer
    */
   public function createCustomer(array $customerData): array
   public function getAdditionalFields(?string $content_type = null):array
   {
      try {
         $response = $this->postRequest('/v2/customers', [
            'given_name' => $customerData['first_name'] ?? '',
            'family_name' => $customerData['last_name'] ?? '',
            'email_address' => $customerData['email'] ?? '',
            'phone_number' => $customerData['phone'] ?? '',
            'reference_id' => $customerData['reference_id'] ?? '',
            'note' => $customerData['note'] ?? ''
         ], $this->environment);
         return [
            'success' => true,
            'data' => $response['customer'] ?? []
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => 'Failed to create customer: ' . $e->getMessage()
         ];
      }
   }
   /**
    * Create a catalog item
    */
   public function createCatalogItem(array $itemData): array
   {
      try {
         $response = $this->postRequest('/v2/catalog/object',  [
            'idempotency_key' => wp_generate_uuid4(),
            'object' => [
               'type' => 'ITEM',
               'id' => '#' . sanitize_title($itemData['name']),
               'item_data' => [
                  'name' => $itemData['name'],
                  'description' => $itemData['description'] ?? '',
                  'variations' => $this->buildItemVariations($itemData)
               ]
            ]
         ], $this->environment);
         return [
            'success' => true,
            'data' => $response['catalog_object'] ?? []
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => 'Failed to create catalog item: ' . $e->getMessage()
         ];
      }
   }
   /**
    * Build item variations for catalog
    */
   protected function buildItemVariations(array $itemData): array
   {
      $variations = [];
      if (isset($itemData['variations']) && is_array($itemData['variations'])) {
         foreach ($itemData['variations'] as $variation) {
            $variations[] = [
               'type' => 'ITEM_VARIATION',
               'id' => '#' . sanitize_title($variation['name']),
               'item_variation_data' => [
                  'name' => $variation['name'],
                  'pricing_type' => 'FIXED_PRICING',
                  'price_money' => [
                     'amount' => $this->formatPrice($variation['_square_price']??0),
                     'currency' => $this->getCurrency()
                  ]
               ]
            ];
         }
      } else {
         // Default single variation
         $variations[] = [
            'type' => 'ITEM_VARIATION',
            'id' => '#regular',
            'item_variation_data' => [
               'name' => 'Regular',
               'pricing_type' => 'FIXED_PRICING',
               'price_money' => [
                  'amount' => $this->formatPrice($itemData['_square_price'] ?? 0),
                  'currency' => $this->getCurrency()
               ]
            ]
         ];
      }
      return $variations;
   }
   /**
    * Handle OAuth disconnect
    */
   public function handleOAuthDisconnect(): array
   {
      try {
         // Revoke the token with Square
         if (!empty($this->credentials['access_token'])) {
            wp_remote_post($this->oauth['revoke'], [
               'body' => [
                  'client_id' => $this->credentials['client_id'] ?? '',
                  'access_token' => $this->credentials['access_token']
               ],
               'headers' => $this->getRequestHeaders()
            ]);
         }
         // Clear stored credentials
         $this->credentials = [
            'client_id' => $this->credentials['client_id'] ?? '',
            'client_secret' => $this->credentials['client_secret'] ?? ''
         ];
         $this->saveCredentials($this->credentials);
         $this->clearCache();
         return [
            'success' => true,
            'message' => 'Successfully disconnected from Square'
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => 'Failed to disconnect: ' . $e->getMessage()
         ];
      }
   }
   /**
    * Register additional WordPress hooks
    */
   protected function registerAdditionalHooks(): void
   {
      if (!$this->isSetUp()) {
         error_log('Square is not setup');
         return;
      }
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
      add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
      add_filter('jvb_checkout_description', function (string $desc, string $provider) {
         if ($provider === 'square') {
            return 'Securely checkout with your name, email, and payments processed by Square.';
         }
         return $desc;
      }, 10, 2);
      // Square-specific pickup fields (extracted from old outputCheckout)
      add_filter('jvb_checkout_fields', [$this, 'addPickupFields'], 10, 2);
      // Browse URL for this client (restaurant menu)
      add_filter('jvb_checkout_browse_url', function () {
         return get_post_type_archive_link(BASE . 'menu_item');
      });
      add_filter('jvb_checkout_browse_text', function () {
         return 'browse our menu';
      });
   }
   /**
    * Pickup/ordering fields for the shared checkout form.
    * Specific to this Square client's food ordering use case.
    */
   public function addPickupFields(string $html, string $provider): string
   {
      if ($provider !== 'square') {
         return $html;
      }
      return $html
         . '<h3>Pickup Details</h3>'
         . Form::render('pickup_time', null, [
            'type'     => 'datetime',
            'label'    => 'Pickup Time',
            'min'      => '11:00',
            'max'      => '20:00',
            'required' => true,
         ])
         . Form::render('special_instructions', null, [
            'type'  => 'textarea',
            'label' => 'Special Instructions',
            'quill' => true,
         ]);
   }
   protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
   {
      $queue    = JVB()->queue();
      $queue->registry()->register(self::$syncCustomer, new TypeConfig(
         executor:   $executor,
         maxRetries: 2
      ));
      $queue->registry()->register(self::$import, new TypeConfig(
         executor:   $executor,
         maxRetries: 3
      ));
   }
   /******************************************************************
    * POST SYNC METHODS
    ******************************************************************/
   /**
    * Handle post save for Square sync
    */
   protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
   {
      error_log('==== [Square]::handleTheSavePost ====');
      $this->queueOperation(self::$syncTo, [
         'items'   => [$postID],
         'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
      ], [
         'priority' => 'high',
         'delay'    => 30,
      ]);
      Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
   }
   /**
    * Handle post deletion
    */
   public function handleDeletePost(int $postID): void
   {
      $item_id = $this->getServiceItemID($postID);
      if (empty($item_id)) {
         return;
      }
      $this->queueOperation(self::$deleteFrom, [
         'external_ids' => [$item_id],
         'post_id'      => $postID,
      ], [
         'priority' => 'high',
      ]);
   }
   /**
    * Process sync to Square
    */
   private function processSyncToSquare(array $data): array
   {
      $items = $data['items'] ?? [];
      $success = [];
      $errors = [];
      if (empty($items)) {
         return [
            'success' => true,
            'result' => [
               'synced' => [],
               'errors' => ['No items to sync']
            ]
         ];
      }
      //Check images are already uploaded first
      $image_mappings = $this->batchUploadImages($items);
      $catalog = [];
      $map = [];
      foreach ($items as $postID) {
         $post = get_post($postID);
         if (!$post) {
            $errors[] = "Post $postID not found";
            continue;
         }
         $square_image_id = $image_mappings[$postID] ?? null;
         $catalog_object = $this->buildCatalogObject($postID, $square_image_id);
         if (is_wp_error($catalog_object)) {
            $errors[] = $catalog_object->get_error_message();
            continue;
         }
         $map[$catalog_object['id']] = $postID;
         $catalog[] = $catalog_object;
      }
      if (!empty($catalog)) {
         $response = $this->postRequest('catalog/batch-upsert', [
            'idempotency_key' => wp_generate_uuid4(),
            'batches' => [[
               'objects' => $catalog
            ]]
         ]);
         if (!is_wp_error($response)) {
            $this->processBatchSyncResponse($response, $map, $success, $errors);
         } else {
            // Handle batch request failure
            $error_message = 'Batch sync failed';
            $this->logError($error_message, [
               'method' => 'processSyncToSquare',
               'post_ids' => $items,
               'error' => $response
            ]);
            // Mark all items as failed
            foreach ($items as $postID) {
               if (get_post($postID)) { // Only if post exists
                  $errors[] = "Failed to sync post $postID";
                  update_post_meta($postID, BASE . '_square_sync_status', 'error');
               }
            }
         }
      }
      return [
         'success' => count($success) > 0,
         'result' => [
            'synced' => $success,
            'errors' => $errors
         ]
      ];
   }
   private function processBatchSyncResponse(array $response, array $map, array &$success, array &$errors):void
   {
      error_log('==== SQUARE::processBatchSyncResponse =====');
      error_log('Full response: '.print_r($response, true));
      // Handle successful objects
      if (!empty($response['objects'])) {
         foreach ($response['objects'] as $object) {
            $object_id = $object['id'];
            $post_id = $map[$object_id] ?? null;
            if (!$post_id) {
               // Try to find post ID from the object ID pattern
               if (preg_match('/#\w+_(\d+)/', $object_id, $matches)) {
                  $post_id = (int)$matches[1];
               }
            }
            if ($post_id && get_post($post_id)) {
               // Update post meta with Square data
               update_post_meta($post_id, BASE . '_square_catalog_id', $object['id']);
               update_post_meta($post_id, BASE . '_square_sync_status', 'synced');
               update_post_meta($post_id, BASE . '_square_last_sync', current_time('mysql'));
               // Save variation IDs if present
               if (!empty($object['item_data']['variations'])) {
                  foreach ($object['item_data']['variations'] as $index => $variation) {
                     update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
                  }
               }
               $success[] = $post_id;
            }
         }
      }
      // Handle errors
      if (!empty($response['errors'])) {
         foreach ($response['errors'] as $error) {
            $error_detail = $error['detail'] ?? 'Unknown error';
            $error_field = $error['field'] ?? '';
            // Try to extract post ID from error field if possible
            $post_id = null;
            if (preg_match('/batches\[0\]\.objects\[(\d+)\]/', $error_field, $matches)) {
               $object_index = (int)$matches[1];
               // Get post ID from the object at this index
               if (isset($catalog_objects[$object_index])) {
                  $object_id = $catalog_objects[$object_index]['id'];
                  $post_id = $map[$object_id] ?? null;
               }
            }
            $error_message = $post_id
               ? "Post $post_id: $error_detail"
               : "Batch error: $error_detail";
            $errors[] = $error_message;
            if ($post_id) {
               update_post_meta($post_id, BASE . '_square_sync_status', 'error');
            }
         }
      }
      // Handle any posts that weren't in the response (shouldn't happen but good to check)
      foreach ($map as $object_id => $post_id) {
         if (!in_array($post_id, $success)) {
            $found_in_errors = false;
            foreach ($errors as $error) {
               if (str_contains($error, "Post $post_id")) {
                  $found_in_errors = true;
                  break;
               }
            }
            if (!$found_in_errors) {
               $errors[] = "Post $post_id: No response from Square";
               update_post_meta($post_id, BASE . '_square_sync_status', 'error');
            }
         }
      }
   }
   /**
    * Build catalog object from WordPress post
    *
    * @param int $postID WordPress post ID
    * @param string|null $square_image_id Previously uploaded Square image ID
    * @return array|WP_Error Catalog object or error
    */
   //TODO: Get to work with Registrar settings
   protected function buildCatalogObject(int $postID, ?string $square_image_id = null): array|WP_Error
   {
      $post = get_post($postID);
      if (!$post) {
         return new WP_Error('post_not_found', "Post $postID not found");
      }
      $meta = Meta::forPost($postID);
      $post_type = get_post_type($postID);
      // Get existing Square catalog ID if it exists
      $existing_square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
      $registrar = Registrar::getInstance($post_type);
      $product_type = 'FOOD_AND_BEV';
      if ($registrar) {
         $conf = $registrar->getIntegration($this->service_name);
         if ($conf) {
            $product_type = $conf->getContentType();
         }
      }
      // Build the base catalog object
      $catalog_object = [
         'type' => 'ITEM',
         'id' => $existing_square_id ?: '#menu_item_item_' . $postID,
         'item_data' => [
            'name' => $post->post_title,
            'description' => wp_strip_all_tags($post->post_content),
            'product_type' => $product_type,
            'variations' => [],
            'is_taxable' => true,
         ]
      ];
      // Add image ID if provided (must be already uploaded to Square)
      if (!empty($square_image_id)) {
         $catalog_object['item_data']['image_ids'] = [$square_image_id];
      }
      // Add variations
      $variations = $meta->get('_square_product_variations');
      if (empty($variations)) {
         // Create default variation if none exist
         $catalog_object['item_data']['variations'][] = [
            'type' => 'ITEM_VARIATION',
            'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
            'item_variation_data' => [
               'name' => 'Regular',
               'ordinal' => 0,
               'pricing_type' => 'FIXED_PRICING',
               'price_money' => [
                  'amount' => $this->formatPrice($meta->get('_square_price')),
                  'currency' => $this->getCurrency()
               ],
               'sellable' => true,
               'stockable' => true
            ]
         ];
      } else {
         $resetVariations = false;
         foreach ($variations as $index => $variation) {
            $id = '#'.BASE.'menu_item_' . $postID . '_var_' . $index;
            if (empty($variation['item_id'])) {
               $resetVariations = true;
               $variations[$index]['item_id'] = $id;
               $variation['item_id'] = $id;
            }
            $catalog_object['item_data']['variations'][] = [
               'type' => 'ITEM_VARIATION',
               'id' => $variation['item_id'],
               'item_variation_data' => [
                  'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
                  'ordinal' => $index,
                  'pricing_type' => 'FIXED_PRICING',
                  'price_money' => [
                     'amount' =>$this->formatPrice($variation['price'] ?? 0),
                     'currency' => $this->getCurrency()
                  ],
                  'sellable' => true,
                  'stockable' => true
               ]
            ];
         }
         if ($resetVariations) {
            $meta->set('_square_product_variations', $variations);
         }
      }
      // Add categories if they exist
      $categories = wp_get_post_terms($postID, $post_type . '_category', ['fields' => 'ids']);
      if (!empty($categories)) {
         $category_ids = [];
         foreach ($categories as $term_id) {
            $square_cat_id = get_term_meta($term_id, BASE . '_square_category_id', true);
            if ($square_cat_id) {
               $category_ids[] = $square_cat_id;
            }
         }
         if (!empty($category_ids)) {
            $catalog_object['item_data']['category_ids'] = $category_ids;
         }
      }
      // Add modifiers if they exist
      $modifiers = $meta->get('modifiers');
      if (!empty($modifiers)) {
         $modifier_ids = [];
         foreach ($modifiers as $modifier) {
            if (!empty($modifier['square_id'])) {
               $modifier_ids[] = $modifier['square_id'];
            }
         }
         if (!empty($modifier_ids)) {
            $catalog_object['item_data']['modifier_list_info'] = array_map(function($id) {
               return ['modifier_list_id' => $id];
            }, $modifier_ids);
         }
      }
      // Add tax settings
      $tax_ids = $meta->get('tax_ids');
      if (!empty($tax_ids)) {
         $catalog_object['item_data']['tax_ids'] = $tax_ids;
      }
      return $catalog_object;
   }
   /**
    * Get variation mapping for post type
    */
   protected function getVariationMapping(string $post_type): array
   {
      $registrar = Registrar::getInstance($post_type);
      if (!$registrar) {
      $this->setContentTypes();
      if (is_null($content_type) || !array_key_exists($content_type, $this->contentTypes)) {
         return [];
      }
      $config = $registrar->getIntegrationConfig($this->service_name);
      $product_type = $config['content_type']??'REGULAR';
      $valid_fields = $this->getValidFieldsForProductType($product_type);
      $defaults = [
         'name' => 'name',
         'id' => '_square_item_id',
         'sku' => '_square_sku',
         'price' => '_square_price',
         'track_inventory' => '_square_track_inventory',
         'service_duration' => '_square_service_duration',
         'available_for_booking' => '_square_available_for_booking',
         'gift_card_type' => '_square_gift_card_type',
         'ingredients' => '_square_ingredients',
         'preparation_time_duration' => '_square_preparation_time_duration'
      ];
      $extended = apply_filters(
         BASE . $this->service_name . '_variation_mapping',
         $defaults,
         $post_type,
         $this
      );
      return array_filter($extended, function($key) use ($valid_fields) {
         return in_array($key, $valid_fields);
      }, ARRAY_FILTER_USE_KEY);
   }
   /**
    * Get or create Square category
    */
   private function getOrCreateSquareCategory(string $name): ?string
   {
      // Check cached mapping
      $cached_id = get_option(BASE . 'square_category_' . sanitize_title($name));
      if ($cached_id) {
         return $cached_id;
      }
      // Create new category
      $response = $this->postRequest('catalog/batch-upsert', [
         'idempotency_key' => wp_generate_uuid4(),
         'batches' => [[
            'objects' => [[
               'type' => 'CATEGORY',
               'id' => '#category_' . sanitize_title($name),
               'category_data' => [
                  'name' => $name
               ]
            ]]
         ]]
      ]);
      if (!is_wp_error($response) && !empty($response['objects'][0]['id'])) {
         $category_id = $response['objects'][0]['id'];
         update_option(BASE . 'square_category_' . sanitize_title($name), $category_id);
         return $category_id;
      }
      return null;
   }
   /**
    * Get field mapping for post type
    */
   protected function getFieldMapping(string $post_type): array
   {
      $registrar = Registrar::getInstance($post_type);
      if (!$registrar) {
         return [];
      }
      $config = $registrar->getIntegrationConfig($this->service_name);
      $product_type = $config['content_type']??'REGULAR';
      $valid_fields = $this->getValidFieldsForProductType($product_type);
      $defaults = [
         'name' => 'post_title',
         'description_html' => 'post_content',
         'abbreviation' => 'abbreviation',
         'id' => '_square_catalog_id',
         'sku' => '_square_sku',
         'category_id' => 'category',
         'image_ids' => 'post_thumbnail',
         'price' => '_square_price',
         'tax_ids' => 'tax_ids',
         // Availability
         'available_online' => '_square_available_online',
         'available_for_pickup' => '_square_available_for_pickup',
         'available_electronically' => '_square_available_electronically',
         // Modifiers
         'modifier_list_info' => 'modifiers',
         // Item options
         'item_options' => 'options',
         // Reporting
         'reporting_category' => 'reporting_category',
      ];
      $extended = apply_filters(
         BASE . $this->service_name . '_field_mapping',
         $defaults,
         $post_type,
         $this
      );
      return array_filter($extended, function($key) use ($valid_fields) {
         return in_array($key, $valid_fields);
      }, ARRAY_FILTER_USE_KEY);
   }
   /**
    * Get valid fields for Square product type
    */
   //TODO: This feels redundant now, with how we've defined fields in getAdditionalFields
   private function getValidFieldsForProductType(string $product_type): array
   {
      $fields = ['name', 'description_html', 'sku', 'price', 'image_ids', 'category_id'];
      $type_specific = [
         'FOOD_AND_BEV' => [
            'ingredients',
            'preparation_time_duration',
            'dietary_preferences',
            'calories_text'
         ],
         'APPOINTMENTS_SERVICE' => [
            'service_duration',
            'available_for_booking',
            'team_member_ids',
            'booking_availability'
         ],
         'EVENT' => [
            'start_date',
            'end_date',
            'venue_details',
            'capacity'
         ],
         'GIFT_CARD' => [
            'gift_card_type',
            'allowed_locations'
         ]
      ];
      if (isset($type_specific[$product_type])) {
         $fields = array_merge($fields, $type_specific[$product_type]);
      }
      return $fields;
   }
   /******************************************************************
    * CUSTOMER MANAGEMENT
    ******************************************************************/
   /**
    * Handle customer authentication during checkout
    */
   //TODO: Is this necessary?
   public function handleCustomerAuth($data):WP_Error|array
   {
      $email = sanitize_email($data['email'] ?? '');
      $action = sanitize_text_field($data['action_type'] ?? 'check');
      if (!$email) {
         return new WP_Error('error', 'Email required');
      }
      switch ($action) {
         case 'check':
            return $this->checkCustomerExists($email);
         case 'create':
            return $this->createCustomerAccount($email);
         default:
            $this->logError('No action set for customer auth: '.$action, [
               'method' => 'handleCustomerAuth'
            ]);
            return new WP_Error('error', 'No action set for customer auth');
      }
   }
   /**
    * Check if customer exists
    */
   private function checkCustomerExists(string $email):WP_Error|array
   {
      // Check WordPress user
      $user = get_user_by('email', $email);
      if ($user) {
         // Check if user has Square customer integration
         $square_customer_id = get_user_meta($user->ID, BASE . '_square_customer_id', true);
         if ($square_customer_id) {
            return [
               'exists' => true,
               'has_account'  => true,
               'message' => 'Account found. Please enter your password to continue'
            ];
         } else {
            return [
               'exists' => true,
               'has_account' => false,
               'message' => 'Email found. Would you like to create an account to save your order history?'
            ];
         }
      }
         // Check Square for customer
         $response = $this->postRequest('customers/search', [
            'filter' => [
               'email_address' => [
                  'exact' => $email
               ]
            ]
         ]);
         if (!is_wp_error($response) && !empty($response['customers'])) {
            return [
               'exists' => true,
               'has_account' => false,
               'square_only' => true,
               'message' => 'Found your previous orders. Create an account to access them?'
            ];
         } else {
            return [
               'exists' => false,
               'message' => 'New customer'
            ];
         }
   }
   private function createCustomerAccount(string $email):WP_Error|array
   {
      // Validate email format
      if (!is_email($email)) {
         return new WP_Error('error', 'Invalid email address');
      }
      // Check if user already exists
      if (email_exists($email)) {
         return [
            'success' => false,
            'exists' => true,
            'message' => 'An account with this email already exists. Please log in instead.'
         ];
      }
      // Create user account without password (they'll set it via email)
      $user_id = wp_create_user(
         $email,
         wp_generate_password(20, true, true), // Temporary random password
         $email
      );
      if (is_wp_error($user_id)) {
         $this->logError('Failed to create customer account', [
            'email' => $email,
            'error' => $user_id->get_error_message()
         ]);
         return new WP_Error('error', 'Failed to create account. Please try again.');
      }
      // Set user role (assuming you have a customer role defined)
      $user = new WP_User($user_id);
      $user->set_role(BASE.'foodie');
      // Generate password reset key
      $reset_key = get_password_reset_key($user);
      if (is_wp_error($reset_key)) {
         return new WP_Error('error', 'Account created, but couldn\'t send email. Please use password reset.');
      }
      // Send welcome email with password setup link
      $this->sendWelcomeEmail($user, $reset_key);
      // Link to Square customer if exists
      $square_customer_id = $this->getOrCreateSquareCustomer([
         'email' => $email,
         'name' => $email
      ]);
      if ($square_customer_id) {
         update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
      }
      return [
         'success' => true,
         'message' => 'Account created! Check your email to set your password.',
         'user_id' => $user_id
      ];
   }
   /**
    * Send welcome email with password setup
    */
   private function sendWelcomeEmail(WP_User $user, string $reset_key): void
   {
      $site_name = get_bloginfo('name');
      $reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
      $message = sprintf(
         "Welcome to %s!\n\n" .
         "Your account has been created. Please click the button below to set your password:\n\n" .
         "%s\n\n" .
         "Or, copy and paste the link below:\n\n".
         "%s\n\n" .
         "Once you've set your password, you can:\n" .
         "- View your order history\n" .
//       "- Save your favorite items\n" .
         "- Speed up checkout with saved payment methods\n\n" .
         "If you didn't create this account, please ignore this email.\n\n" .
         "Thanks,\n",
         $site_name,
         JVB()->email()->button('Reset Password', $reset_url),
         JVB()->email()->link($reset_url),
      );
      JVB()->email()->sendEmail(
         $user->user_email,
         sprintf('[%s] Welcome! Set Your Password', $site_name),
         $message
      );
   }
   /******************************************************************
    * WEBHOOK HANDLING
    ******************************************************************/
   /**
    * Validate webhook signature
    */
   protected function validateWebhook(array $payload): bool
   {
      // Get signature from headers
      $signature = $_SERVER['HTTP_X_SQUARE_SIGNATURE'] ?? '';
      // If no signature provided by Square, validation fails
      if (empty($signature)) {
         $this->logDebug('No webhook signature provided by Square');
         return false;
      }
      // If webhook signature key is not configured, we can't validate
      // but we might want to process the webhook anyway (less secure)
      if (empty($this->webhook_signature_key)) {
         $this->logDebug('Webhook signature key not configured - processing without validation');
         // You might want to return false here for stricter security
         // return false;
         return true; // Process webhook but log that it's unverified
      }
      // Get the raw request body
      $body = file_get_contents('php://input');
      // Calculate expected signature
      $expected = base64_encode(hash_hmac('sha256', $body, $this->webhook_signature_key, true));
      // Use timing-safe comparison
      $is_valid = hash_equals($expected, $signature);
      if (!$is_valid) {
         $this->logError('Invalid webhook signature', [
            'provided' => substr($signature, 0, 10) . '...',
            'expected' => substr($expected, 0, 10) . '...'
         ]);
      }
      return $is_valid;
   }
   /**
    * Process webhook event
    */
   protected function processWebhook(array $payload): bool
   {
      $event_type = $payload['type'] ?? '';
      $data = $payload['data'] ?? [];
      switch ($event_type) {
         case 'payment.created':
         case 'payment.updated':
            return $this->handlePaymentWebhook($data);
         case 'order.created':
         case 'order.updated':
         case 'order.fulfillment.updated':
            return $this->handleOrderWebhook($data);
         case 'catalog.version.updated':
            return $this->handleCatalogWebhook($data);
         case 'customer.created':
         case 'customer.updated':
            return $this->handleCustomerWebhook($data);
         default:
            $this->logDebug('Unhandled webhook type', ['type' => $event_type]);
            return true;
      }
   }
   /**
    * Handle order status webhook
    */
   private function handleOrderWebhook(array $data): bool
   {
      $order_id = $data['object']['order']['id'] ?? '';
      $state = $data['object']['order']['state'] ?? '';
      $fulfillments = $data['object']['order']['fulfillments'] ?? [];
      if (!$order_id) {
         return false;
      }
      // Find the WP post for this order
      $wp_order_id = $this->getOrderPost($order_id);
      if ($wp_order_id) {
         // Update the post meta
         $meta = Meta::forPost($wp_order_id);
         $updates = [
            'status' => $state,
            'updated_at' => current_time('mysql')
         ];
         // Extract fulfillment status and pickup time
         if (!empty($fulfillments[0])) {
            $fulfillment = $fulfillments[0];
            $updates['fulfillment_status'] = $fulfillment['state'] ?? $state;
            if (!empty($fulfillment['pickup_details']['pickup_at'])) {
               $updates['pickup_time'] = $fulfillment['pickup_details']['pickup_at'];
            }
         }
         $meta->setAll($updates);
         // Trigger notification to customer if order is ready
         if ($state === 'PREPARED') {
            do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
         }
      }
      // Trigger action for other integrations
      do_action(BASE . 'square_order_updated', $order_id, $state, $data);
      return true;
   }
   /******************************************************************
    * CONNECTION TESTING
    ******************************************************************/
   protected function performConnectionTest(): bool
   {
      try {
         $response = $this->getRequest('/v2/merchants/me');
         if (is_wp_error($response)) {
            $this->logError('Connection test failed', ['error' => $response->get_error_message()]);
            return false;
         }
         // Check if we got valid merchant data
         return !empty($response['merchant']);
      } catch (Exception $e) {
         $this->logError('Connection test failed', ['error' => $e->getMessage()]);
         return false;
      }
   }
   /**
    * Check OAuth connection status
    */
   protected function checkOAuthStatus(): array
   {
      if (!$this->hasOAuthCredentials()) {
         return [
            'success' => false,
            'message' => 'OAuth not configured. Please authorize with Square.',
            'authorized' => false
         ];
      }
      // Test the connection
      if ($this->performConnectionTest()) {
         $merchant = $this->getRequest('merchants/me');
         return [
            'success' => true,
            'message' => 'Successfully connected to Square',
            'authorized' => true,
            'merchant' => $merchant['merchant'] ?? []
         ];
      }
      return [
         'success' => false,
         'message' => 'OAuth token may be expired. Please re-authorize.',
         'authorized' => false
      ];
   }
   /******************************************************************
    * ADMIN UI
    ******************************************************************/
   /**
    * Enqueue checkout scripts with Square configuration
    */
   public function enqueueScripts(): void
   {
      jvbInlineStyles('forms');
      $this->loadCredentials();
      $sdk_url = $this->environment === 'production'
         ? 'https://web.squarecdn.com/v1/square.js'
         : 'https://sandbox.web.squarecdn.com/v1/square.js';
      wp_enqueue_script(
         'square-payments-sdk',
         $sdk_url,
         [],
         null,
         ['strategy' => 'defer', 'in_footer' => true]
      );
      // Shared cart checkout base class
      wp_register_script(
         'jvb-checkout',
         JVB_URL . 'assets/js/min/checkout.min.js',
         ['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup', 'jvb-login'],
         '1.1.32',
         ['strategy' => 'defer', 'in_footer' => true]
      );
      // Square checkout extends CartCheckout
      wp_register_script(
         'jvb-square-checkout',
         JVB_URL . 'assets/js/min/square.min.js',
         ['jvb-checkout', 'square-payments-sdk'],
         '1.1.32',
         ['strategy' => 'defer', 'in_footer' => true]
      );
      wp_enqueue_script('jvb-square-checkout');
      wp_localize_script('jvb-square-checkout', 'squareConfig', [
         'isOpen'         => jvbIsOpen()?'1':'0',
         'application_id' => $this->credentials['client_id'] ?? '',
         'location_id'    => $this->locationId,
         'environment'    => $this->environment,
         'currency'       => $this->getCurrency(),
         'is_logged_in'   => is_user_logged_in(),
         'user_email'     => is_user_logged_in() ? wp_get_current_user()->user_email : '',
      ]);
   }
   /******************************************************************
    * HELPER METHODS
    ******************************************************************/
   /**
    * Get or create Square customer
    */
   public function getOrCreateSquareCustomer(array $customer_info): ?string
   {
      if (empty($customer_info['email'])) {
         return null;
      }
      // Search for existing customer
      $search_response = $this->postRequest('customers/search', [
         'filter' => [
            'email_address' => [
               'exact' => $customer_info['email']
            ]
         ]
      ]);
      if (!is_wp_error($search_response) && !empty($search_response['customers'])) {
         return $search_response['customers'][0]['id'];
      }
      // Create new customer
      $create_response = $this->postRequest('customers', [
         'given_name' => $customer_info['name'] ?? '',
         'email_address' => $customer_info['email'],
         'phone_number' => $customer_info['phone'] ?? ''
      ]);
      if (!is_wp_error($create_response) && !empty($create_response['customer']['id'])) {
         return $create_response['customer']['id'];
      }
      return null;
   }
   /**
    * Process delete from Square
    */
   private function processDeleteFromSquare(array $data): array
   {
      $square_ids = $data['square_ids'] ?? [];
      if (empty($square_ids)) {
         return [
            'success' => false,
            'result' => ['error' => 'No Square IDs provided']
         ];
      }
      $response = $this->postRequest('catalog/batch-delete', [
         'object_ids' => $square_ids
      ]);
      if (is_wp_error($response)) {
         return [
            'success' => false,
            'result' => ['error' => $response->get_error_message()]
         ];
      }
      // Clean up post meta
      if (!empty($data['post_id'])) {
         delete_post_meta($data['post_id'], BASE . '_square_catalog_id');
         delete_post_meta($data['post_id'], BASE . '_square_sync_status');
      }
      return [
         'success' => true,
         'result' => ['deleted' => $square_ids]
      ];
   }
   /**
    * Process sync from Square
    */
   private function processSyncFromSquare(array $data): array
   {
      $cursor = $data['cursor'] ?? null;
      $types = $data['types'] ?? ['ITEM'];
      $request_data = [
         'types' => implode(',', $types),
         'limit' => 100
      ];
      if ($cursor) {
         $request_data['cursor'] = $cursor;
      }
      $response = $this->getRequest('catalog/list?' . http_build_query($request_data));
      if (is_wp_error($response)) {
         return [
            'success' => false,
            'result' => ['error' => $response->get_error_message()]
         ];
      }
      $imported = 0;
      $errors = [];
      foreach ($response['objects'] ?? [] as $object) {
         if ($object['type'] === 'ITEM') {
            $result = $this->importSquareItem($object);
            if ($result) {
               $imported++;
            } else {
               $errors[] = $object['id'];
            }
         }
      }
      // Queue next batch if cursor exists
      if (!empty($response['cursor'])) {
         $this->queueOperation('sync_from_square', [
            'cursor' => $response['cursor'],
            'types' => $types
         ]);
      }
      return [
         'success' => true,
         'result' => [
            'imported' => $imported,
            'errors' => $errors,
            'has_more' => !empty($response['cursor'])
         ]
      ];
   }
   /**
    * Import Square item to WordPress
    */
   private function importSquareItem(array $item): bool|int
   {
      //TODO: We need to add the post type to custom meta for Square, this is not good if we have multiple post types with the same product type
      // Find matching content type
      $product_type = $item['item_data']['product_type'] ?? 'REGULAR';
      $post_type = null;
      foreach (Registrar::getRegistered() as $registrar) {
         if (!$registrar->hasIntegration($this->service_name)) {
            continue;
         }
         $config = $registrar->getIntegration($this->service_name);
         if ($config->getContent_type() && $config->getContent_type() === $product_type) {
            $post_type = jvbCheckBase($registrar->getSlug());
            break;
         }
      }
      if (!$post_type) {
         return false;
      }
      // Check if item already exists
      $existing = get_posts([
         'post_type' => $post_type,
         'meta_key' => BASE . '_square_catalog_id',
         'meta_value' => $item['id'],
         'posts_per_page' => 1
      ]);
      $post_data = [
         'post_title' => $item['item_data']['name'] ?? '',
         'post_content' => $item['item_data']['description'] ?? '',
         'post_type' => $post_type,
         'post_status' => 'publish'
      ];
      if (!empty($existing)) {
         $post_data['ID'] = $existing[0]->ID;
         $post_id = wp_update_post($post_data);
      } else {
         $post_id = wp_insert_post($post_data);
      }
      if (!$post_id || is_wp_error($post_id)) {
         return false;
      }
      // Map and save meta fields
      $this->mapSquareFieldsToWordPress($post_id, $item);
      return $post_id;
   }
   /**
    * Map Square fields to WordPress meta
    */
   private function mapSquareFieldsToWordPress(int $post_id, array $item): void
   {
      $meta = Meta::forPost($post_id);
      $field_map = $this->getFieldMapping(get_post_type($post_id));
      $values_to_save = [];
      // Save Square ID
      $values_to_save['_square_catalog_id'] = $item['id'];
      $values_to_save['_square_last_sync'] = current_time('mysql');
      // Handle variations if present
      if (!empty($item['item_data']['variations'])) {
         $variations_data = [];
         foreach ($item['item_data']['variations'] as $index => $variation) {
            $var_data = [
               'name' => $variation['item_variation_data']['name'] ?? '',
               'sku' => $variation['item_variation_data']['_square_sku'] ?? '',
            ];
            // Extract price
            if (!empty($variation['item_variation_data']['price_money'])) {
               $var_data['price'] = $variation['item_variation_data']['price_money']['amount'] / 100;
            }
            // Extract other variation fields
            $variation_map = $this->getVariationMapping(get_post_type($post_id));
            foreach ($variation_map as $square_field => $wp_field) {
               if (isset($variation['item_variation_data'][$square_field])) {
                  $var_data[$wp_field] = $variation['item_variation_data'][$square_field];
               }
            }
            $variations_data[] = $var_data;
            // Save variation Square ID
            update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
         }
         $values_to_save['product_variations'] = $variations_data;
      }
      // Map other fields
      foreach ($field_map as $square_field => $wp_field) {
         if ($square_field !== 'price' && isset($item['item_data'][$square_field])) {
            $values_to_save[$wp_field] = $item['item_data'][$square_field];
         }
      }
      // Save all values at once
      $meta->setAll($values_to_save);
   }
   /**
    * Process customer sync
    */
   private function processSyncCustomer(array $data): array
   {
      $user_id = $data['user_id'] ?? 0;
      if (!$user_id) {
         return [
            'success' => false,
            'result' => ['error' => 'No user ID provided']
         ];
      }
      $user = get_user_by('ID', $user_id);
      if (!$user) {
         return [
            'success' => false,
            'result' => ['error' => 'User not found']
         ];
      }
      // Get or create Square customer
      $square_customer_id = $this->getOrCreateSquareCustomer([
         'email' => $user->user_email,
         'name' => $user->display_name
      ]);
      if ($square_customer_id) {
         update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
         return [
            'success' => true,
            'result' => ['customer_id' => $square_customer_id]
         ];
      }
      return [
         'success' => false,
         'result' => ['error' => 'Could not sync customer']
      ];
   }
   /**
    * Handle catalog webhook
    */
   private function handleCatalogWebhook(array $data): bool
   {
      // Queue sync from Square for updated items
      $this->queueOperation('import_catalog', [
         'types' => ['ITEM'],
         'updated_at' => $data['object']['catalog_version']['updated_at'] ?? null
      ], [
         'delay' => 60 // Wait a minute to batch multiple updates
      ]);
      return true;
   }
   /**
    * Handle customer webhook
    */
   private function handleCustomerWebhook(array $data): bool
   {
      $square_customer_id = $data['object']['customer']['id'] ?? '';
      $email = $data['object']['customer']['email_address'] ?? '';
      if (!$square_customer_id || !$email) {
         return false;
      }
      // Find WordPress user with this Square customer ID
      $users = get_users([
         'meta_key' => BASE . '_square_customer_id',
         'meta_value' => $square_customer_id,
         'number' => 1
      ]);
      if (!empty($users)) {
         // Update user meta with latest Square data
         $user = $users[0];
         update_user_meta($user->ID, BASE . '_square_customer_updated', current_time('mysql'));
         // Clear cached customer data
         $this->cache->forget('square_customer_' . $user->ID);
      }
      return true;
   }
   /**
    * Handle payment webhook
    */
   private function handlePaymentWebhook(array $data): bool
   {
      $payment_id = $data['object']['payment']['id'] ?? '';
      $order_id = $data['object']['payment']['order_id'] ?? '';
      $status = $data['object']['payment']['status'] ?? '';
      if (!$payment_id) {
         return false;
      }
      if ($order_id) {
         $order = $this->getOrderPost($order_id);
         if ($order) {
            Meta::forPost($order)->set('square_payment_status', $status);
         }
      }
      // Trigger action for other integrations
      do_action(BASE . 'square_payment_updated', $payment_id, $status, $order_id, $data);
      return true;
   }
   /**
    * Get the appropriate API base URL
    */
   protected function getApiUrl(string $endpoint, ?string $baseKey = null): string
   {
      return rtrim($this->apiBase[$this->environment], '/') . '/' . ltrim($endpoint, '/');
   }
   /**
    * Handle importing catalog from Square
    */
   protected function handleImportFromSquare(): array
   {
      if (!$this->locationId) {
         return [
            'success' => false,
            'message' => 'Please select a location first'
         ];
      }
      $this->queueOperation('import_catalog', [
         'types' => ['ITEM'],
         'user_id' => $this->userID
      ], [
         'priority' => 'normal'
      ]);
      return [
         'success' => true,
         'message' => 'Import from Square queued'
      ];
   }
   /**
    * Handle syncing to Square
    */
   protected function handleSyncToSquare(): array
   {
      if (!$this->locationId) {
         return [
            'success' => false,
            'message' => 'Please select a location first'
         ];
      }
      $post_types = array_map(function($type) {
         return jvbCheckBase($type);
      }, $this->syncPostTypes);
      // Get all posts to sync
      $posts = get_posts([
         'post_type' => $post_types,
         'posts_per_page' => -1,
         'post_status' => 'publish'
      ]);
      $post_ids = wp_list_pluck($posts, 'ID');
      // Queue sync operation
      $this->queueOperation('sync_to_square', [
         'items' => $post_ids,
         'user_id' => $this->userID
      ], [
         'priority' => 'normal'
      ]);
      return [
         'success' => true,
         'message' => sprintf('Queued %d items for sync to Square', count($post_ids))
      ];
   }
   /**
    * Refresh category mappings from Square
    */
   protected function handleRefreshCategories(): array
   {
      // Refresh category mappings from Square
      $response = $this->getRequest('catalog/list?types=CATEGORY');
      if (!is_wp_error($response) && isset($response['objects'])) {
         $count = 0;
         foreach ($response['objects'] as $category) {
            if ($category['type'] === 'CATEGORY') {
               $name = $category['category_data']['name'] ?? '';
               if ($name) {
                  update_option(
                     BASE . 'square_category_' . sanitize_title($name),
                     $category['id']
                  );
                  $count++;
               }
            }
         }
         return [
            'success' => true,
            'message' => sprintf('Refreshed %d categories', $count),
            'count' => $count
         ];
      } else {
         return [
            'success' => false,
            'message' => 'Failed to refresh categories'
         ];
      }
   }
   /**
    * Get available Square locations
    */
   protected function handleGetLocations(): array
   {
      // Make sure we have an access token
      if (empty($this->access_token)) {
         return [
            'success' => false,
            'message' => 'Access token required to fetch locations'
         ];
      }
      // Fetch available Square locations
      $response = $this->getRequest('locations');
      if (!is_wp_error($response) && isset($response['locations'])) {
         $locations = array_map(function($loc) {
            return [
               'id' => $loc['id'],
               'name' => $loc['name'] ?? 'Unnamed Location',
               'address' => $loc['address']['address_line_1'] ?? '',
               'status' => $loc['status'] ?? 'ACTIVE'
            ];
         }, $response['locations']);
         // Filter to only active locations
         $active_locations = array_filter($locations, function($loc) {
            return $loc['status'] === 'ACTIVE';
         });
         // Format for select field
         if (isset($_REQUEST['for_select'])) {
            $options = [];
            foreach ($active_locations as $location) {
               $label = $location['name'];
               if ($location['address']) {
                  $label .= ' - ' . $location['address'];
               }
               $options[$location['id']] = $label;
            }
            return [
               'success' => true,
               'options' => $options
            ];
         }
         return [
            'success' => true,
            'locations' => array_values($active_locations)
         ];
      }
      return [
         'success' => false,
         'message' => 'Failed to fetch locations. Please check your access token and environment settings.'
      ];
   }
   protected function validateCredentials(array $credentials): bool
   {
      // For OAuth services, we need different validation based on setup stage
      if ($this->isOAuthService) {
         // Stage 1: Initial setup (need app credentials)
         $hasAppCredentials = !empty($credentials['client_id'])
            && !empty($credentials['client_secret']);
         if (!$hasAppCredentials) {
            $this->logError('Missing required Square application credentials', [
               'has_app_id' => !empty($credentials['client_id']),
               'has_app_secret' => !empty($credentials['client_secret'])
            ]);
            return false;
         }
         // Stage 2: After OAuth (should have access token)
         // Access token might not exist yet if we're just saving app credentials
         // before OAuth flow, so we only validate format if it exists
         if (!empty($credentials['access_token'])) {
            // Validate token format (Square tokens are long alphanumeric strings)
            if (!is_string($credentials['access_token']) ||
               strlen($credentials['access_token']) < 20) {
               $this->logError('Invalid access token format');
               return false;
            }
            // Validate merchant_id if present (should be set after OAuth)
            if (!empty($credentials['merchant_id'])) {
               if (!is_string($credentials['merchant_id']) ||
                  strlen($credentials['merchant_id']) < 10) {
                  $this->logError('Invalid merchant ID format');
                  return false;
               }
            }
         }
         // Validate environment setting
         if (isset($credentials['environment'])) {
            $validEnvironments = ['sandbox', 'production'];
            if (!in_array($credentials['environment'], $validEnvironments)) {
               $this->logError('Invalid environment setting', [
                  'provided' => $credentials['environment'],
                  'valid' => $validEnvironments
               ]);
               return false;
            }
         }
         return true;
      }
      // Fallback for non-OAuth (shouldn't happen for Square, but good practice)
      return !empty($credentials);
   }
   protected function sanitizeCredentials(array $credentials): array
   {
      $sanitized = [];
      // Define which fields should be sanitized and how
      $text_fields = [
         'client_id',
         'client_secret',
         'access_token',
         'refresh_token',
         'merchant_id',
         'location_id',
         'webhook_signature_key'
      ];
      foreach ($credentials as $key => $value) {
         // Handle text fields - trim whitespace but preserve case
         if (in_array($key, $text_fields)) {
            if (is_string($value)) {
               $sanitized[$key] = trim($value);
            } else {
               $sanitized[$key] = $value;
            }
         }
         // Handle environment field
         elseif ($key === 'environment') {
            $sanitized[$key] = sanitize_key($value);
         }
         // Handle boolean fields
         elseif ($key === 'use_sandbox' || str_starts_with($key, 'enable_')) {
            $sanitized[$key] = (bool) $value;
         }
         // Handle numeric fields
         elseif ($key === 'expires_at' || $key === 'updated_at' || $key === 'expires_in') {
            $sanitized[$key] = is_numeric($value) ? (int) $value : $value;
         }
         // Handle arrays (like locations)
         elseif (is_array($value)) {
            $sanitized[$key] = array_map('sanitize_text_field', $value);
         }
         // Default sanitization for other string fields
         else {
            if (is_string($value)) {
               $sanitized[$key] = sanitize_text_field($value);
            } else {
               $sanitized[$key] = $value;
            }
         }
      }
      return $sanitized;
   }
   public function getServiceDescription(): string
   {
      return "Connect with Square for payment processing, inventory management, and customer synchronization.";
   }
   public function setContentTypes(): void
   {
      $this->has_content = true;
      $this->defaultContent = 'REGULAR';
      $base = $this->setBaseFields();
      $this->contentTypes = [
         'REGULAR'   => $base,
         'FOOD_AND_BEV' => array_merge($base, $this->setFoodAndBevFields()),
         'APPOINTMENTS_SERVICE' => array_merge($this->setAppointmentServiceFields()),
         'EVENT'        => array_merge($this->setEventFields()),
         'GIFT_CARD' => array_merge($this->setGiftCardFields())
      ];
   }
   public function getAdditionalFields(?string $content_type = null):array {
      if ($content_type === 'customer') {
         return $this->getCustomerFields();
      }
      if ($content_type && array_key_exists($content_type, $this->contentTypes)){
         $array = $this->contentTypes[$content_type];
         return array_combine(
            array_map(fn($k) => '_square_' . $k, array_keys($array)),
            $array
         );
      } else if ($content_type && !array_key_exists($content_type, $this->contentTypes)) {
         error_log('Could not get default fields for '.$this->service_name.' content type: '.$content_type);
         return [];
      }
      $array = $this->setBaseFields();
        return array_combine(
            array_map(fn($k) => '_square_' . $k, array_keys($array)),
            $array
        );
   }
   protected function getCustomerFields():array
   {
      return [
         'customer_id'  => [
            'type'   => 'text',
            'label'  => 'Square Customer ID',
            'hidden'=> true,
            'section'=> 'your-account'
         ],
         'address_line_1'  => [
            'type'   => 'text',
            'label'  => 'Address Line 1',
            'hint'   => 'ex: 6551 111 St NW',
            'required'  => true,
            'section'   => 'address'
         ],
         'address_line_2'  => [
            'type'   => 'text',
            'label'  => 'Address Line 2',
            'hint'   => 'ex: Unit 2',
            'section'   => 'address'
         ],
         'city'   => [
            'type'   => 'text',
            'label'=> 'City',
            'section'   => 'address',
            'required'  => true,
         ],
         'state' => [
            'type'   => 'text',
            'label' => 'Province',
            'hint' => 'The two-character code, example: AB',
            'default'=> 'AB',
            'section'   => 'address',
            'required'  => true,
         ],
         'zip_code' => [
            'type'   => 'text',
            'label'  => 'Postal Code',
            'section'=> 'address'
         ],
         'countryCode'  => [
            'type'   => 'text',
            'label'  => 'Country Code',
            'hint'   => 'The tw-character country code, example: CA',
            'default'   => 'CA',
            'section'   => 'address',
            'required'  => true,
         ]
      ];
   }
   protected function setBaseFields():array
   {
      return [
         'price' => [
            'type'        => 'number',
            'label'       => 'Price',
            'step'        => 0.01,
            'max'         => 99999,
            'description' => 'Price for this variation'
         ],
         'sku' => [
            'type'        => 'text',
            'label'       => 'SKU',
            'description' => 'Stock keeping unit'
         ],
         'cart_quantity'   => [
            'type'   => 'number',
            'label'  => 'Quantity',
            'hidden' => true,
         ],
         'available_for_pickup' => [
            'type'   => 'true_false',
            'label'  => 'Available for Pick Up',
            'section'=> 'square-advanced'
         ],
         'available_online' => [
            'type'   => 'true_false',
            'label'  => 'Available online',
            'section'=> 'square-advanced'
         ],
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product (sizes, colors, etc.)',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->setVariationFields()
         ],
      ];
   }
   protected function setVariationFields(?string $type = null):array
   {
      $fields = [
         'name' => [
            'type'        => 'text',
            'label'       => 'Variation Name',
            'description' => 'e.g., "Small", "Large", "Red", etc.'
         ],
            'sku' => [
                'type'        => 'text',
                'label'       => 'SKU',
                'description' => 'Stock keeping unit'
            ],
         'price' => [
            'type'        => 'number',
            'label'       => 'Price',
            'step'        => 0.01,
            'max'         => 99999,
            'description' => 'Price for this variation'
         ],
         'track_inventory' => [
            'type'   => 'true_false',
            'label'  => 'Track Inventory',
         ],
         'item_id' => [
            'type'        => 'text',
            'label'       => 'Square Variation ID',
            'description' => 'Square catalog ID for this variation',
            'hidden'      => true
         ],
         'last_sync' => [
            'type'   => 'datetime',
            'label'  => 'Last Sync',
            'hidden' => true
         ]
      ];
      switch ($type) {
         case 'FOOD_AND_BEV':
            $fields['ingredients']  = [
               'type'   => 'textarea',
               'label'  => 'Ingredients List',
               'description'  => 'Separate ingredients with commas',
            ];
            $fields['preparation_time_duration']   = [
               'type'   => 'number',
               'label'  => 'Preparation time (in minutes)',
            ];
            break;
         case 'GIFT_CARD':
            $fields['gift_card_type']  = [
               'type'   => 'select',
               'label'  => 'Gift Card Type',
               'options'   => [
                  'PHYSICAL'  => 'Physical',
                  'DIGITAL'   => 'Digital',
               ],
               'default'   => 'DIGITAL',
            ];
            break;
         case 'APPOINTMENTS_SERVICE':
            $fields['service_duration']   = [
               'type'   => 'number',
               'label'  => 'Duration of Service in Minutes'
            ];
            $fields['available_for_booking'] = [
               'type'   => 'true_false',
               'label'  => 'Available for Booking'
            ];
            break;
      }
      return array_combine(
            array_map(fn($k) => '_square_' . $k, array_keys($fields)),
            $fields
        );
   }
   protected function setFoodAndBevFields():array
   {
      return [
         'ingredients'  => [
            'type'   => 'textarea',
            'label'  => 'Ingredients',
            'hint'   => 'A comma separated list of ingredients'
         ],
         'preparation_time_duration' => [
            'label'  => 'Preparation Time',
            'type'   => 'number',
            'hint'   => 'Preparation time in minutes'
         ],
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product (sizes, colors, etc.)',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->setVariationFields('FOOD_AND_BEV')
         ],
      ];
      /* TODO: Set definitions for:
         'dietary_preferences'   => [
         ],
         'calories_text'   => [
         ],
      */
   }
   protected function setAppointmentServiceFields():array
   {
      return [
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product (sizes, colors, etc.)',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->setVariationFields('APPOINTMENTS_SERVICE')
         ],
      ];
   }
   protected function setEventFields():array
   {
      return [
         'venue_details'   => [
            'type'   => 'textarea',
            'label'  => 'Venue Details',
         ],
         'capacity'  => [
            'type'   => 'number',
            'label'  => 'Capacity'
         ],
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product (sizes, colors, etc.)',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->setVariationFields('EVENT')
         ],
      ];
   }
   protected function setGiftCardFields():array
   {
      return [
//       'allowed_locations'  => [
//
//       ],
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product (sizes, colors, etc.)',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->setVariationFields('GIFT_CARD')
         ],
      ];
   }
   /*********************************************************
   IMAGE PROCESSING
    *********************************************************/
   /**
    * Upload image file to Square
    *
    * @param int $imgID WordPress attachment ID
    * @return array|WP_Error Upload result or error
    */
   protected function uploadImageToSquare(int $imgID): array|WP_Error
   {
      $supported_image_id = $this->getSupportedImage($imgID);
      // Check if already uploaded
      $existing_square_image_id = $this->getSquareImageId($supported_image_id);
      if ($existing_square_image_id) {
         return [
            'success' => true,
            'image_id' => $existing_square_image_id
         ];
      }
      $file_path = get_attached_file($supported_image_id);
      if (!file_exists($file_path)) {
         return new WP_Error('file_not_found', 'Image file not found');
      }
      // Verify file type
      $mime_type = get_post_mime_type($supported_image_id);
      if (!in_array($mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
         return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
      }
      $image_title = get_the_title($supported_image_id);
      $alt_text = get_post_meta($supported_image_id, '_wp_attachment_image_alt', true);
      // Build multipart request - SINGLE STEP
      $boundary = wp_generate_password(24);
      $headers = $this->getRequestHeaders();
      $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
      // Request JSON part
      $request_json = [
         'idempotency_key' => wp_generate_uuid4(),
         'image' => [
            'type' => 'IMAGE',
            'id' => '#IMAGE_' . $supported_image_id . '_' . time(),
            'image_data' => [
               'name' => $image_title ?: 'Image',
               'caption' => $alt_text ?: ''
            ]
         ],
         'object_id' => $supported_image_id
      ];
      $body = $this->buildMultipartBody($file_path, $request_json, $boundary);
      $response = wp_remote_post(
         $this->getApiUrl('v2/catalog/images'),
         [
            'headers' => $headers,
            'body' => $body,
            'timeout' => 60
         ]
      );
      if (is_wp_error($response)) {
         return $response;
      }
      $data = json_decode(wp_remote_retrieve_body($response), true);
      if (!empty($data['errors'])) {
         return new WP_Error('upload_error', $data['errors'][0]['detail'] ?? 'Unknown error');
      }
      if (!empty($data['image']['id'])) {
         $this->setSquareImageId($supported_image_id, $data['image']['id']);
         return [
            'success' => true,
            'image_id' => $data['image']['id']
         ];
      }
      return new WP_Error('upload_failed', 'Failed to upload image');
   }
   protected function buildMultipartBody(string $file_path, array $request_json, string $boundary): string
   {
      $eol = "\r\n";
      $body = '';
      // Add request JSON part
      $body .= '--' . $boundary . $eol;
      $body .= 'Content-Disposition: form-data; name="request"' . $eol;
      $body .= 'Content-Type: application/json' . $eol . $eol;
      $body .= json_encode($request_json) . $eol;
      // Add image file part
      $filename = basename($file_path);
      $file_contents = file_get_contents($file_path);
      $mime_type = mime_content_type($file_path);
      $body .= '--' . $boundary . $eol;
      $body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
      $body .= 'Content-Type: ' . $mime_type . $eol . $eol;
      $body .= $file_contents . $eol;
      $body .= '--' . $boundary . '--' . $eol;
      return $body;
   }
   /**
    * Attach image to catalog item
    *
    * @param int $postID WordPress post ID
    * @param string $square_image_id Square image ID
    * @return array|WP_Error Result or error
    */
   public function attachImageToCatalogItem(int $postID, string $square_image_id): array|WP_Error
   {
      $square_catalog_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
      if (!$square_catalog_id) {
         return new WP_Error('no_catalog_id', 'Post has not been synced to Square');
      }
      // Get the current catalog item
      $response = $this->getRequest('catalog/object/' . $square_catalog_id);
      if (is_wp_error($response)) {
         return $response;
      }
      if (empty($response['object'])) {
         return new WP_Error('item_not_found', 'Catalog item not found in Square');
      }
      $catalog_object = $response['object'];
      // Add image to item data
      $catalog_object['item_data']['image_ids'] = [$square_image_id];
      // Update the catalog item
      $update_response = $this->postRequest('catalog/batch-upsert', [
         'idempotency_key' => wp_generate_uuid4(),
         'batches' => [[
            'objects' => [$catalog_object]
         ]]
      ]);
      if (is_wp_error($update_response)) {
         return $update_response;
      }
      return [
         'success' => true,
         'message' => 'Image attached to catalog item'
      ];
   }
   /**
    * Get stored Square image ID for an attachment
    *
    * @param int $attachment_id WordPress attachment ID
    * @return string|false Square image ID or false if not found
    */
   public function getSquareImageId(int $attachment_id): string|false
   {
      $image_id = get_post_meta($attachment_id, BASE . 'square_image_id', true);
      if ($image_id) {
         // Verify it still exists in Square
         return $this->verifySquareImage($image_id) ? $image_id : false;
      }
      return false;
   }
   /**
    * Store Square image ID for an attachment
    *
    * @param int $attachment_id WordPress attachment ID
    * @param string $square_image_id Square image ID
    * @return bool Success
    */
   public function setSquareImageId(int $attachment_id, string $square_image_id): bool
   {
      return update_post_meta($attachment_id, BASE . 'square_image_id', $square_image_id);
   }
   /**
    * Verify image exists in Square catalog
    *
    * @param string $square_image_id Square image ID
    * @return bool Whether image exists
    */
   protected function verifySquareImage(string $square_image_id): bool
   {
      $response = $this->getRequest('catalog/object/' . $square_image_id);
      if (is_wp_error($response)) {
         return false;
      }
      return !empty($response['object']) && $response['object']['type'] === 'IMAGE';
   }
   /**
    * Batch upload images for multiple posts
    *
    * @param array $post_ids Array of WordPress post IDs
    * @return array Results for each post
    */
   private function batchUploadImages(array $post_ids): array
   {
      $image_mappings = [];
      $image_objects = [];
      // First pass: collect images that need uploading
      foreach ($post_ids as $post_id) {
         $thumbnail_id = get_post_thumbnail_id($post_id);
         if (!$thumbnail_id) {
            $image_mappings[$post_id] = null;
            continue;
         }
         // Check if already uploaded and still exists in Square
         $existing_square_id = get_post_meta($thumbnail_id, BASE . '_square_image_id', true);
         if ($existing_square_id && $this->verifySquareImage($existing_square_id)) {
            $image_mappings[$post_id] = $existing_square_id;
            continue;
         }
         // Get supported image format (handles WebP conversion)
         $supported_image_id = $this->getSupportedImage($thumbnail_id);
         if (!$supported_image_id) {
            $image_mappings[$post_id] = null;
            continue;
         }
         // Upload the image to Square using the proper endpoint
         $square_image_id = $this->uploadSingleImageToSquare($supported_image_id, $post_id);
         if ($square_image_id && !is_wp_error($square_image_id)) {
            $image_mappings[$post_id] = $square_image_id;
            // Store the Square image ID for future reference
            update_post_meta($thumbnail_id, BASE . '_square_image_id', $square_image_id);
         } else {
            $this->logError('Failed to upload image for post', [
               'post_id' => $post_id,
               'thumbnail_id' => $thumbnail_id,
               'error' => is_wp_error($square_image_id) ? $square_image_id->get_error_message() : 'Unknown error'
            ]);
            $image_mappings[$post_id] = null;
         }
      }
      return $image_mappings;
   }
   /**
    * Upload a single image to Square using the CreateCatalogImage endpoint
    *
    * @param int $attachment_id WordPress attachment ID
    * @param int $post_id Associated post ID
    * @return string|WP_Error Square image ID or error
    */
   private function uploadSingleImageToSquare(int $attachment_id, int $post_id): string|WP_Error
   {
      $file_path = get_attached_file($attachment_id);
      if (!file_exists($file_path)) {
         return new WP_Error('file_not_found', 'Image file not found');
      }
      // Verify file type
      $mime_type = get_post_mime_type($attachment_id);
      $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
      if (!in_array($mime_type, $allowed_types)) {
         return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
      }
      // Prepare multipart form data
      $boundary = wp_generate_password(24);
      $headers = $this->getRequestHeaders();
      $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
      // Build the request body
      $eol = "\r\n";
      $body = '';
      // Add request JSON
      $request_data = [
         'idempotency_key' => wp_generate_uuid4(),
         'image' => [
            'type' => 'IMAGE',
            'id' => '#TEMP_IMAGE_' . $attachment_id . '_' . time(),
            'image_data' => [
               'name' => get_the_title($attachment_id) ?: basename($file_path),
               'caption' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true) ?:
                  sprintf('Image for %s', get_the_title($post_id))
            ]
         ]
      ];
      $body .= '--' . $boundary . $eol;
      $body .= 'Content-Disposition: form-data; name="request"' . $eol;
      $body .= 'Content-Type: application/json' . $eol . $eol;
      $body .= json_encode($request_data) . $eol;
      // Add image file
      $filename = basename($file_path);
      $file_contents = file_get_contents($file_path);
      $body .= '--' . $boundary . $eol;
      $body .= 'Content-Disposition: form-data; name="image"; filename="' . $filename . '"' . $eol;
      $body .= 'Content-Type: ' . $mime_type . $eol . $eol;
      $body .= $file_contents . $eol;
      // End boundary
      $body .= '--' . $boundary . '--' . $eol;
      // Make the request to the CreateCatalogImage endpoint
      $response = wp_remote_post(
         $this->getApiUrl('catalog/images'),
         [
            'headers' => $headers,
            'body' => $body,
            'timeout' => 60
         ]
      );
      if (is_wp_error($response)) {
         return $response;
      }
      $response_body = wp_remote_retrieve_body($response);
      $data = json_decode($response_body, true);
      if (!empty($data['errors'])) {
         $error_message = $data['errors'][0]['detail'] ?? 'Unknown error';
         return new WP_Error('upload_error', $error_message);
      }
      if (!empty($data['image']['id'])) {
         $this->logDebug('Successfully uploaded image to Square', [
            'post_id' => $post_id,
            'attachment_id' => $attachment_id,
            'square_image_id' => $data['image']['id']
         ]);
         return $data['image']['id'];
      }
      return new WP_Error('upload_failed', 'Failed to get image ID from Square response');
   }
   protected function handleApiError(int $code, string $body, string $endpoint): void
   {
      parent::handleApiError($code, $body, $endpoint);
      $decoded = json_decode($body, true);
      $errorCode = $decoded['errors'][0]['code'] ?? '';
      // Handle Square-specific OAuth errors
      if ($errorCode === 'ACCESS_TOKEN_EXPIRED') {
         $this->logDebug('Access token expired, attempting refresh');
         if ($this->refreshOAuthToken()) {
            // Token refreshed, could retry the request
            $this->logDebug('Token refreshed successfully');
         }
      } elseif ($errorCode === 'UNAUTHORIZED' || $errorCode === 'ACCESS_TOKEN_REVOKED') {
         $this->logError('Square authorization revoked or invalid', [
            'error_code' => $errorCode,
            'user_id' => $this->userID
         ]);
         // Clear invalid credentials
         $this->deleteCredentials();
      }
   }
   public function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
   {
      // Build line items for Square
      $line_items = [];
      foreach ($items as $item) {
         $line_item = [
            'quantity' => (string)$item['quantity'], // MUST be string!
         ];
         // Use catalog_object_id if available (recommended)
         if (!empty($item['catalog_object_id'])) {
            $line_item['catalog_object_id'] = $item['catalog_object_id'];
            $line_item['catalog_version'] = $item['catalog_version'] ?? null;
         } else {
            // Ad-hoc line item (not recommended - no tax/inventory automation)
            $line_item['name'] = $item['name'];
            $line_item['base_price_money'] = [
               'amount' => (int)$item['_square_price'],
               'currency' => $this->getCurrency()
            ];
         }
         if (!empty($item['note'])) {
            $line_item['note'] = $item['note'];
         }
         $line_items[] = $line_item;
      }
      $order_data = [
         'idempotency_key' => wp_generate_uuid4(), // Different from payment idempotency key
         'order' => [
            'location_id' => $this->locationId,
            'line_items' => $line_items,
            'state' => 'OPEN'
         ]
      ];
      // Add customer if available
      if ($customer_id) {
         $order_data['order']['customer_id'] = $customer_id;
      }
      // Add metadata
      if (!empty($data['note'])) {
         $order_data['order']['metadata'] = [
            'special_instructions' => $data['note']
         ];
      }
      if (!empty($data['pickup_time'])) {
         $order_data['order']['metadata']['pickup_time'] = $data['pickup_time'];
      }
      return $this->postRequest('orders', $order_data);
   }
   public function createSquarePayment(
      string $source_id,
      string $idempotency_key,
      int $amount_cents,
      string $order_id,
      ?string $customer_id
   ): array|WP_Error
   {
      $payment_data = [
         'idempotency_key' => $idempotency_key,
         'source_id' => $source_id,
         'amount_money' => [
            'amount' => $amount_cents, // Already in cents!
            'currency' => $this->getCurrency()
         ],
         'order_id' => $order_id,
         'location_id' => $this->locationId,
         'autocomplete' => true, // Capture immediately
      ];
      // Add customer if available
      if ($customer_id) {
         $payment_data['customer_id'] = $customer_id;
      }
      // Add reference ID for tracking
      $payment_data['reference_id'] = 'WP_' . time();
      return $this->postRequest('payments', $payment_data);
   }
   public function saveOrderToWordPress(array $order_data): int
   {
      // Extract customer info
      $customer_email = $order_data['customer']['email'] ?? '';
      $customer_name = $order_data['customer']['name'] ?? '';
      // Find or create WP user for logged-in association
      $user_id = 0;
      if ($customer_email) {
         $user = get_user_by('email', $customer_email);
         if ($user) {
            $user_id = $user->ID;
            // Store Square customer ID on user
            if (!empty($order_data['square_customer_id'])) {
               update_user_meta($user_id, BASE . '_square_customer_id', $order_data['square_customer_id']);
            }
         }
      }
      // Create order post
      $order_post_id = wp_insert_post([
         'post_type' => BASE.$this->orderPostType,
         'post_title' => 'Order #' . $order_data['square_order_id'],
         'post_status' => 'publish',
         'post_author' => $user_id // Associate with user if logged in
      ]);
      if (!$order_post_id || is_wp_error($order_post_id)) {
         $this->logError('Failed to create order post', ['order_data' => $order_data]);
         return 0;
      }
      // Save all order meta
      $meta = Meta::forPost($order_post_id);
      $fields = $this->getOrderFields();
      unset($fields['post_title']);
      $meta->setAll([
         'square_order_id' => $order_data['square_order_id'],
         'square_payment_id' => $order_data['square_payment_id'] ?? '',
         'square_customer_id' => $order_data['square_customer_id'] ?? '',
         'amount' => $order_data['amount'],
         'status' => $order_data['status'],
         'fulfillment_status' => $order_data['fulfillment_status'] ?? 'PROPOSED',
         'pickup_time' => $order_data['pickup_time'] ?? '',
         'customer_email' => $customer_email,
         'customer_name' => $customer_name,
         'customer_phone' => $order_data['customer']['phone'] ?? '',
         'special_instructions' => $order_data['note'] ?? '',
         'items' => $order_data['items'],
         'receipt_url' => $order_data['receipt_url'] ?? '',
         'created_at' => current_time('mysql'),
         'updated_at' => current_time('mysql')
      ]);
      return $order_post_id;
   }
   /**
    * Get currency code
    */
   private function getCurrency(): string
   {
      return get_option(BASE . 'currency', 'CAD');
   }
   /**
    * Get customer with saved cards (2025-compliant)
    */
   public function getUserCards(string $customer_id): array
   {
      $response = $this->getRequest('cards?customer_id=' . $customer_id);
      return $response['cards'] ?? [];
   }
   public function getUserOrders(string $customer_email): array
   {
      // First get Square customer ID
      $customer_response = $this->postRequest('customers/search', [
         'filter' => [
            'email_address' => ['exact' => $customer_email]
         ]
      ]);
      $customer_id = $customer_response['customers'][0]['id'] ?? null;
      if (!$customer_id) {
         return [];
      }
      // Get their orders
      $orders_response = $this->postRequest('orders/search', [
         'filter' => [
            'customer_filter' => [
               'customer_ids' => [$customer_id]
            ]
         ],
         'sort' => [
            'sort_field' => 'CREATED_AT',
            'sort_order' => 'DESC'
         ],
         'limit' => 50
      ]);
      return $orders_response['orders'] ?? [];
   }
   public function checkOrderStatus(string $order_id): ?string
   {
      // Fetch from Square
      $response = $this->getRequest('orders/' . $order_id);
      if (!is_wp_error($response)) {
         return $response['order']['state'] ?? null;
      }
      return null;
   }
   /**
    * Single-item sync. Called by IntegrationExecutor::processSyncTo().
    * Delegates to syncBatchToService since Square uses batch-upsert.
    */
   public function syncPostToService(int $postID): array|WP_Error
   {
      return $this->syncBatchToService(['items' => [$postID]]);
   }
   /**
    * Batch sync — preferred by IntegrationExecutor when available.
    * Wraps existing processSyncToSquare which already handles batches.
    */
   public function syncBatchToService(array $data): array|WP_Error
   {
      $result = $this->processSyncToSquare($data);
      if (empty($result['success'])) {
         $errors = implode(', ', $result['result']['errors'] ?? ['Sync failed']);
         return new WP_Error('square_sync_failed', $errors);
      }
      return $result;
   }
   /**
    * Delete catalog object from Square.
    * Called by IntegrationExecutor::processDeleteFrom().
    */
   public function deleteFromService(string $externalId): array|WP_Error
   {
      $result = $this->processDeleteFromSquare(['square_ids' => [$externalId]]);
      if (empty($result['success'])) {
         return new WP_Error('square_delete_failed', $result['result']['error'] ?? 'Delete failed');
      }
      return $result;
   }
   /**
    * Import from Square catalog → WordPress.
    * Called by IntegrationExecutor::processImport().
    */
   public function importFromService(array $data): array|WP_Error
   {
      $result = $this->processSyncFromSquare($data);
      if (empty($result['success'])) {
         return new WP_Error('square_import_failed', $result['result']['error'] ?? 'Import failed');
      }
      return $result;
   }
   /**
    * Sync customer to Square.
    * Called by IntegrationExecutor::processSyncCustomer().
    */
   public function syncCustomer(array $data): array|WP_Error
   {
      $result = $this->processSyncCustomer($data);
      if (empty($result['success'])) {
         return new WP_Error('square_customer_sync_failed', $result['result']['error'] ?? 'Customer sync failed');
      }
      return $result;
   }
   public function addDashboardPages():void
   {
      $page = JVB()->dashboard()->addPage('Your Orders', 'orders', 'receipt');
      $page->setScripts(['jvb-crud']);
   }
   public function renderDashPage():void
   {
      $this->determineUser();
      $crud = new CRUDSkeleton();
      $crud->icon('receipt');
      $crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like');
      $crud->content($this->orderPostType, 'Order', 'Orders');
      $crud->addSearch();
      $crud->addCapabilities(['view']);
      $crud->setEmptyState(sprintf(
         '<div class="empty-state">
            <h3>%sNothing here%s</h3>
            <p>It doesn\'t look like you have any orders yet.</p>
            <p>Head on over to <a href="%s">our menu</a> and make your first order!</p>
         </div>',
         jvbDashIcon($crud->getIcon()),
         jvbDashIcon($crud->getIcon()),
         get_post_type_archive_link(BASE.'menu_item')
      ));
      $crud->render();
      return $this->contentTypes[$content_type];
   }
   public function getOrderPost(string $order_id):int|false
   {
      $posts = new WP_Query([
         'post_type'       => BASE.$this->orderPostType,
         'posts_per_page'  => 1,
         'meta_key'        => BASE.'square_order_id',
         'meta_value'      => $order_id,
         'fields'       => 'ids',
      ]);
      wp_reset_postdata();
      return $posts->have_posts() ? $posts[0] : false;
   }
   public function getOrderHistory(int $user_id):array
   {
      $posts = new WP_Query([
         'post_type'       => BASE.$this->orderPostType,
         'posts_per_page'  => 25,
         'author'       => $user_id,
         'orderby'         => 'date',
         'order'           => 'desc',
         'fields'       => 'ids',
      ]);
      wp_reset_postdata();
      return array_map(function ($post) {
         $fields = Meta::forPost($post);
         $fields['wp_order_id'] = $post;
         return $fields;
      }, $posts->posts);
   }
   public function formatPrice(string|float|int $priceValue):int
   {
      //Convert dollars to cents
      return intval(floatval($priceValue) * 100);
   }
}