| | |
| | | namespace JVBase\integrations\services; |
| | | use Exception; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\integrations\APIEndpoint; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | Webhooks; |
| | | |
| | | protected string $orderPostType = '_square_order'; |
| | | protected string $apiVersion = '2025-09-24'; |
| | | public array $apiEndpoints = [ |
| | | |
| | | ]; |
| | | |
| | | protected function __construct() |
| | | { |
| | | $this->service_name = 'square'; |
| | | $this->title = 'Square'; |
| | | $this->icon = 'square-logo'; |
| | | $this->apiVersion = '2026-07-15'; |
| | | $this->supportsWebp = false; |
| | | |
| | | $this->canSync = [ |
| | |
| | | 'update' => true, |
| | | 'delete' => true, |
| | | ]; |
| | | $this->hasBatchCreate = true; |
| | | $this->hasBatchUpdate = true; |
| | | $this->hasBatchDelete = true; |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | |
| | | $locs->setHidden(); |
| | | $locs->setOptions($this->getLocationOptions()); |
| | | |
| | | $loc = $this->addField('location_id', 'Default Location', 'select', 'manage_options'); |
| | | $loc->setOptions($this->loadCredentials()['locations']); |
| | | $loc = $this->addField('location_id', 'Set Location', 'select', 'manage_options'); |
| | | $loc->setOptions($this->loadCredentials()['locations']??[]); |
| | | $loc->setHint('Select default Square location for transactions'); |
| | | } |
| | | |
| | |
| | | add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']); |
| | | } |
| | | |
| | | public function addDashboardPages():void |
| | | { |
| | | $page = Dashboard::getInstance()->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(); |
| | | } |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | // TODO: Implement initialize() method. |
| | | $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 |
| | | { |
| | |
| | | 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); |
| | | return match ($endpoint) { |
| | | '/v2/customers/search' => $this->handleEmailSearchResponse($response, $method), |
| | | '/v2/customers' => $this->handleCreateUserResponse($response, $method), |
| | | '/v2/locations' => $this->handleGetLocationResponse($response, $method), |
| | | default => $this->response(false, 'Unhandled Response for ' . $endpoint . ' endpoint, with method: ' . $method), |
| | | }; |
| | | } |
| | | |
| | | protected function setContentTypes(): void |
| | | { |
| | | // TODO: Implement setContentTypes() method. |
| | | $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 $response['id']; |
| | | 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/v2/customers',$this->getBaseURL()), |
| | | sprintf( |
| | | '%s%s', |
| | | $this->getBaseURL(), |
| | | $this->apiEndpoints['customer']['create']->getEndpoint() |
| | | ), |
| | | $data |
| | | ); |
| | | } catch (Exception $e) { |
| | |
| | | } |
| | | 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 |
| | | { |
| | |
| | | ********************************************************************/ |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $queue->registry()->register( |
| | | self::$syncCustomer, |
| | | new TypeConfig( |
| | |
| | | { |
| | | return $this->loadCredentials()['location_id']??false; |
| | | } |
| | | protected function getLocations():array|false |
| | | public function getLocations():array|false |
| | | { |
| | | if (!$this->getAccessToken()) { |
| | | return false; |
| | |
| | | ? 'https://connect.squareup.com' |
| | | : 'https://connect.squareupsandbox.com'; |
| | | } |
| | | protected function getAccessToken():string |
| | | { |
| | | return $this->loadCredentials()['access_token']??''; |
| | | } |
| | | |
| | | /*************************************************************** |
| | | * ACTIONS |
| | | ***************************************************************/ |
| | |
| | | } |
| | | } |
| | | |
| | | public function getAdditionalFields(?string $content_type = null):array |
| | | { |
| | | $this->setContentTypes(); |
| | | if (is_null($content_type) || !array_key_exists($content_type, $this->contentTypes)) { |
| | | return []; |
| | | } |
| | | return $this->contentTypes[$content_type]; |
| | | } |
| | | |
| | | public function handleImportFromSquare():array |
| | | { |
| | | |
| | | return $this->response(true, 'Did a thing'); |
| | | } |
| | | |
| | | public function handleSyncToSquare():array |
| | | { |
| | | |
| | | return $this->response(true, 'Did a thing'); |
| | | } |
| | | |
| | | |
| | | } |