| | |
| | | */ |
| | | class Square extends Integrations |
| | | { |
| | | protected static string $syncCustomer = 'square_sync_customer'; |
| | | protected array $allowedContent = [ |
| | | 'REGULAR', |
| | | 'FOOD_AND_BEV', |
| | |
| | | public static function getInstance(?int $userID = null):self |
| | | { |
| | | $key = is_null($userID) ? 'base' : $userID; |
| | | |
| | | if (!array_key_exists($key, self::$instances)) { |
| | | self::$instances[$key] = new static($userID); |
| | | self::$instances[$key] = new self($userID); |
| | | } |
| | | return self::$instances[$key]; |
| | | } |
| | | |
| | | protected function __construct(?int $userID = null) |
| | | { |
| | | |
| | | // Display properties |
| | | $this->title = 'Square'; |
| | | $this->icon = 'square-logo'; |
| | |
| | | 'name' => $variation['name'], |
| | | 'pricing_type' => 'FIXED_PRICING', |
| | | 'price_money' => [ |
| | | 'amount' => intval($variation['_square_price'] * 100), // Convert to cents |
| | | 'currency' => 'USD' |
| | | 'amount' => $this->formatPrice($variation['_square_price']??0), |
| | | 'currency' => $this->getCurrency() |
| | | ] |
| | | ] |
| | | ]; |
| | |
| | | 'name' => 'Regular', |
| | | 'pricing_type' => 'FIXED_PRICING', |
| | | 'price_money' => [ |
| | | 'amount' => intval(($itemData['_square_price'] ?? 0) * 100), |
| | | 'currency' => 'USD' |
| | | 'amount' => $this->formatPrice($itemData['_square_price'] ?? 0), |
| | | 'currency' => $this->getCurrency() |
| | | ] |
| | | ] |
| | | ]; |
| | |
| | | return; |
| | | } |
| | | |
| | | add_action('wp_login', [$this, 'trackUserLogin'], 10, 2); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); |
| | | |
| | | add_filter('jvbAdditionalActions', [Checkout::class, 'render']); |
| | |
| | | add_filter('jvb_checkout_browse_text', function () { |
| | | return 'browse our menu'; |
| | | }); |
| | | // Register queue executor types |
| | | add_action('init', [$this, 'registerQueueTypes'], 10); |
| | | } |
| | | |
| | | /** |
| | |
| | | ]); |
| | | } |
| | | |
| | | public function registerQueueTypes(): void |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $executor = new IntegrationExecutor(); |
| | | |
| | | $queue->registry()->register(self::$syncTo, new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'items', |
| | | chunkSize: 50, |
| | | maxRetries: 3 |
| | | )); |
| | | |
| | | $queue->registry()->register(self::$deleteFrom, new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'external_ids', |
| | | chunkSize: 200, |
| | | maxRetries: 2 |
| | | )); |
| | | |
| | | $queue->registry()->register(self::$syncFrom, new TypeConfig( |
| | | executor: $executor, |
| | | maxRetries: 3 |
| | | )); |
| | | |
| | | $queue->registry()->register(self::$syncCustomer, new TypeConfig( |
| | | executor: $executor, |
| | |
| | | */ |
| | | protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void |
| | | { |
| | | error_log('==== [Square]::handleTheSavePost ===='); |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'items' => [$postID], |
| | | 'user_id' => $this->userID, |
| | | 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | |
| | | update_post_meta($postID, BASE . '_square_sync_status', 'queued'); |
| | | Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function handleDeletePost(int $postID): void |
| | | { |
| | | $square_id = get_post_meta($postID, BASE . '_square_catalog_id', true); |
| | | $item_id = $this->getServiceItemID($postID); |
| | | |
| | | if ($square_id) { |
| | | if (empty($item_id)) { |
| | | return; |
| | | } |
| | | $this->queueOperation(self::$deleteFrom, [ |
| | | 'external_ids' => [$square_id], |
| | | 'external_ids' => [$item_id], |
| | | 'post_id' => $postID, |
| | | ], [ |
| | | 'priority' => 'high', |
| | | ]); |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | 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) { |
| | |
| | | |
| | | // 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 = [ |
| | |
| | | 'item_data' => [ |
| | | 'name' => $post->post_title, |
| | | 'description' => wp_strip_all_tags($post->post_content), |
| | | 'product_type' => 'FOOD_AND_BEV', |
| | | 'product_type' => $product_type, |
| | | 'variations' => [], |
| | | 'is_taxable' => true, |
| | | ] |
| | |
| | | } |
| | | |
| | | // Add variations |
| | | $variations = $meta->get('product_variations'); |
| | | $variations = $meta->get('_square_product_variations'); |
| | | if (empty($variations)) { |
| | | // Create default variation if none exist |
| | | $price = floatval($meta->get('price') ?: 0); |
| | | $catalog_object['item_data']['variations'][] = [ |
| | | 'type' => 'ITEM_VARIATION', |
| | | 'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default', |
| | |
| | | 'ordinal' => 0, |
| | | 'pricing_type' => 'FIXED_PRICING', |
| | | 'price_money' => [ |
| | | 'amount' => intval($price * 100), // Convert dollars to cents |
| | | 'currency' => 'CAD' |
| | | 'amount' => $this->formatPrice($meta->get('_square_price')), |
| | | 'currency' => $this->getCurrency() |
| | | ], |
| | | 'sellable' => true, |
| | | 'stockable' => true |
| | | ] |
| | | ]; |
| | | } else { |
| | | $resetVariations = false; |
| | | foreach ($variations as $index => $variation) { |
| | | $existing_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true); |
| | | $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' => $existing_var_id ?: '#'.BASE.'menu_item_' . $postID . '_var_' . $index, |
| | | 'id' => $variation['item_id'], |
| | | 'item_variation_data' => [ |
| | | 'name' => $variation['name'] ?? 'Variation ' . ($index + 1), |
| | | 'ordinal' => $index, |
| | | 'pricing_type' => 'FIXED_PRICING', |
| | | 'price_money' => [ |
| | | 'amount' => intval(floatval($variation['price'] ?? 0) * 100), |
| | | 'currency' => 'CAD' |
| | | '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 |
| | |
| | | return $catalog_object; |
| | | } |
| | | |
| | | /** |
| | | * Build variations from repeater field |
| | | */ |
| | | private function buildVariations(int $postID, array $values, string $post_type): array |
| | | { |
| | | $variations = []; |
| | | $product_variations = $values['product_variations'] ?? []; |
| | | |
| | | // Get variation field mapping |
| | | $variation_map = $this->getVariationMapping($post_type); |
| | | |
| | | // If we have repeater variations |
| | | if (!empty($product_variations) && is_array($product_variations)) { |
| | | foreach ($product_variations as $index => $variation_data) { |
| | | // Skip empty variations |
| | | if (empty($variation_data['name']) && empty($variation_data['_square_sku'])) { |
| | | continue; |
| | | } |
| | | |
| | | $variation = [ |
| | | 'type' => 'ITEM_VARIATION', |
| | | 'id' => '#' . $post_type . '_' . $postID . '_var_' . $index, |
| | | 'item_variation_data' => [ |
| | | 'name' => $variation_data['name'] ?? 'Variation ' . ($index + 1), |
| | | 'ordinal' => $index, |
| | | 'pricing_type' => 'FIXED_PRICING' |
| | | ] |
| | | ]; |
| | | |
| | | // Check for existing Square variation ID |
| | | $square_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true); |
| | | if ($square_var_id) { |
| | | $variation['id'] = $square_var_id; |
| | | } |
| | | |
| | | // Map variation fields |
| | | foreach ($variation_map as $square_field => $wp_field) { |
| | | if (isset($variation_data[$wp_field])) { |
| | | switch ($square_field) { |
| | | case 'price': |
| | | $variation['item_variation_data']['price_money'] = [ |
| | | 'amount' => intval($variation_data[$wp_field] * 100), // Convert to cents |
| | | 'currency' => 'CAD' |
| | | ]; |
| | | break; |
| | | |
| | | case 'sku': |
| | | $variation['item_variation_data']['_square_sku'] = $variation_data[$wp_field]; |
| | | break; |
| | | |
| | | case 'track_inventory': |
| | | $variation['item_variation_data']['track_inventory'] = (bool)$variation_data[$wp_field]; |
| | | break; |
| | | |
| | | case 'service_duration': |
| | | if (!empty($variation_data[$wp_field])) { |
| | | $variation['item_variation_data']['service_data'] = [ |
| | | 'duration_minutes' => intval($variation_data[$wp_field]) |
| | | ]; |
| | | } |
| | | break; |
| | | |
| | | default: |
| | | $variation['item_variation_data'][$square_field] = $variation_data[$wp_field]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | $variations[] = $variation; |
| | | } |
| | | } |
| | | |
| | | // If no variations exist, create a default one from base price |
| | | if (empty($variations) && !empty($values['price'])) { |
| | | $variations[] = [ |
| | | 'type' => 'ITEM_VARIATION', |
| | | 'id' => '#' . $post_type . '_' . $postID . '_var_default', |
| | | 'item_variation_data' => [ |
| | | 'name' => 'Regular', |
| | | 'ordinal' => 0, |
| | | 'pricing_type' => 'FIXED_PRICING', |
| | | 'price_money' => [ |
| | | 'amount' => intval($values['price'] * 100), |
| | | 'currency' => 'CAD' |
| | | ] |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | return $variations; |
| | | } |
| | | |
| | | /** |
| | | * Get variation mapping for post type |
| | |
| | | ]; |
| | | } |
| | | |
| | | // Generate username from email |
| | | $username = sanitize_user(current(explode('@', $email))); |
| | | $username = $this->generateUniqueUsername($username); |
| | | |
| | | // Create user account without password (they'll set it via email) |
| | | $user_id = wp_create_user( |
| | | $username, |
| | | $email, |
| | | wp_generate_password(20, true, true), // Temporary random password |
| | | $email |
| | | ); |
| | |
| | | |
| | | // Set user role (assuming you have a customer role defined) |
| | | $user = new \WP_User($user_id); |
| | | $user->set_role(BASE.'foodie'); // Or whatever role |
| | | $user->set_role(BASE.'foodie'); |
| | | |
| | | // Generate password reset key |
| | | $reset_key = get_password_reset_key($user); |
| | |
| | | // Link to Square customer if exists |
| | | $square_customer_id = $this->getOrCreateSquareCustomer([ |
| | | 'email' => $email, |
| | | 'name' => $username |
| | | 'name' => $email |
| | | ]); |
| | | |
| | | if ($square_customer_id) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Generate unique username |
| | | */ |
| | | private function generateUniqueUsername(string $base): string |
| | | { |
| | | $username = $base; |
| | | $counter = 1; |
| | | |
| | | while (username_exists($username)) { |
| | | $username = $base . $counter; |
| | | $counter++; |
| | | } |
| | | |
| | | return $username; |
| | | } |
| | | |
| | | /** |
| | | * Send welcome email with password setup |
| | | */ |
| | | private function sendWelcomeEmail(\WP_User $user, string $reset_key): void |
| | |
| | | "%s\n\n" . |
| | | "Once you've set your password, you can:\n" . |
| | | "- View your order history\n" . |
| | | "- Save your favorite items\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", |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Track user login for security |
| | | */ |
| | | public function trackUserLogin(string $user_login, \WP_User $user): void |
| | | { |
| | | // Check if user has Square integration |
| | | $role = jvbUserRole($user->ID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if ($registrar) { |
| | | $config = $registrar->getIntegration($this->service_name); |
| | | if ($config->isCustomer()) { |
| | | $login_count = (int)get_user_meta($user->ID, BASE . '_square_login_count', true); |
| | | $login_count++; |
| | | |
| | | update_user_meta($user->ID, BASE . '_square_login_count', $login_count); |
| | | update_user_meta($user->ID, BASE . '_square_last_login', current_time('mysql')); |
| | | |
| | | // Check if password reset is needed |
| | | if ($login_count % self::PASSWORD_RESET_INTERVAL === 0) { |
| | | $this->schedulePasswordReset($user->ID); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Schedule password reset for security |
| | | */ |
| | | private function schedulePasswordReset(int $user_id): void |
| | | { |
| | | update_user_meta($user_id, BASE . '_square_password_reset_required', true); |
| | | |
| | | // Send notification |
| | | $user = get_user_by('ID', $user_id); |
| | | if ($user) { |
| | | JVB()->email()->sendEmail( |
| | | $user->user_email, |
| | | '['.get_bloginfo('name').'] Security Code', |
| | | 'For your security, enter this code to continue accessing your account and saved payment methods.', |
| | | ); |
| | | } |
| | | } |
| | | |
| | | /****************************************************************** |
| | | * WEBHOOK HANDLING |
| | | ******************************************************************/ |
| | |
| | | }, $posts->posts); |
| | | |
| | | } |
| | | |
| | | public function formatPrice(string|float|int $priceValue):int |
| | | { |
| | | //Convert dollars to cents |
| | | return intval(floatval($priceValue) * 100); |
| | | } |
| | | } |