Jake Vanderwerf
2026-02-08 df6c00db050e188a6bd5707e72c4f1f331ced923
inc/integrations/Helcim.php
@@ -1,1633 +1,1345 @@
<?php
namespace JVBase\integrations;
use JVBase\meta\Meta;
use Exception;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_Post;
use JVBase\ui\Checkout;
use JVBase\managers\queue\TypeConfig;
use JVBase\managers\queue\executors\IntegrationExecutor;
if (!defined('ABSPATH')) {
   exit;
}
/**
 * Helcim Integration for JVBase
 * Handles bidirectional sync, customer management, and order processing
 * Helcim Integration Class
 *
 * Handles HelcimPay.js checkout, invoice retrieval, customer/card management,
 * and bidirectional product sync via API token authentication.
 *
 * Helcim is the source of truth for invoices and orders.
 * Products sync bidirectionally through the standard integration field flow.
 *
 * @since 1.0.0
 */
class Helcim extends Integrations
{
   // Helcim-specific configuration
   private string $api_token;
   private string $account_id;
   private string $terminal_id;
   private string $webhook_secret;
   protected string $service_name = 'helcim';
   protected string|array $apiBase = 'https://api.helcim.com/v2';
   // Field mapping cache
   private array $field_mappings = [];
   private array $category_cache = [];
   protected bool $isOAuthService = false;
   // Order processing
   private bool $is_test_mode = false;
   private array $payment_form_settings = [];
   // User security
   private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins
   /**
    * Helcim API rate limits
    * @see https://devdocs.helcim.com/docs/api-rate-limits
    */
   protected array $rate_limits = [
      'per_second' => 5,
      'per_minute' => 60,
      'per_hour'   => 1000
   ];
   public function __construct(?int $userID = null)
   {
      $this->service_name = 'helcim';
      $this->title = 'Helcim';
      $this->icon = 'currency-circle-dollar';
      $this->icon  = 'credit-card';
      // Helcim API endpoints
      $this->apiBase = [
         'production' => 'https://api.helcim.com/v2',
         'sandbox' => 'https://api-sandbox.helcim.com/v2'
      $this->fields = [
         'api_token' => [
            'type'     => 'text',
            'subtype'  => 'password',
            'label'    => 'API Token',
            'hint'     => 'Found in Helcim Dashboard → Settings → API Access',
            'required' => true,
         ],
         'currency' => [
            'type'    => 'select',
            'label'   => 'Currency',
            'options' => [
               'CAD' => 'CAD',
               'USD' => 'USD',
            ],
            'default' => 'CAD',
         ],
      ];
      $this->apiEndpoints = [
         'commerce/invoice',
         'commerce/transaction',
         'commerce/customer',
         'commerce/card-batch',
         'commerce/terminal',
         'commerce/product',
         'commerce/order',
         'payment/purchase',
         'payment/verify',
         'payment/capture',
         'payment/refund',
         'customer/create',
         'customer/update',
         'customer/get',
         'inventory/product',
         'inventory/batch'
      $this->advanced = [
         'fee_saver' => [
            'type'  => 'true_false',
            'label' => 'Fee Saver',
            'hint'  => 'Pass processing fees to customers (not compatible with Google Pay)',
         ],
         'allow_ach' => [
            'type'  => 'true_false',
            'label' => 'Allow ACH/Bank Payments',
            'hint'  => 'Enable bank account payments alongside credit card',
         ],
      ];
      $this->instructions = [
         'Go to <a href="https://my.helcim.com" target="_blank">Helcim Dashboard</a>',
         'Navigate to Settings → API Access',
         'Create a new API Access Configuration',
         'Enable permissions: General (Customers, Invoices, Products), Transaction Processing',
         'Copy the API Token and paste it below',
      ];
      $this->canSync = [
         'create' => true,
         'update' => true,
         'delete' => true
         'delete' => false,
      ];
      $this->fields = [
         'test_mode' => [
            'type'   => 'select',
            'label'  => 'Environment',
            'options'   => [
               '1'   => 'Test Mode',
               '0'   => 'Production'
            ],
            'default'   => '1'
         ],
         'api_token' => [
            'type'   => 'text',
            'subtype'   => 'password',
            'required'  => true,
            'hint'   => 'Your Helcim API Token'
         ],
         'account_id'   => [
            'type'   => 'text',
            'required'  => true,
            'label'  => 'Account ID',
            'hint'   => 'Your Helcim Account ID',
         ],
         'webhook_secret'  => [
            'type'   => 'text',
            'subtype'   => 'password',
            'label'  => 'Webhook Secret',
            'hint'   => 'For webhook verification',
            'required'  => true,
         ]
      ];
      $this->advanced = [
      ];
      $this->instructions = [
         'Once you are set up, add this URL to your Helcim webhook settings: <code>'.esc_html(rest_url('jvb/v1/webhooks/helcim')).'</code>'
      ];
      $this->defaults = [
      ];
      $this->handleWebhooks = true;
      $this->handleWebhooks = false;
      parent::__construct($userID);
      $this->actions = array_merge(
         $this->actions,
         [
            'import_from_helcim' => 'handleImportFromHelcim',
            'sync_to_helcim'     => 'handleSyncToHelcim'
         ]
      );
      // Initialize field mappings
      $this->initializeFieldMappings();
      // Helcim-specific actions (processAction dispatches these)
      $this->actions = array_merge($this->actions, [
         'initialize_checkout' => 'initializeCheckout',
         'get_invoices'        => 'handleGetInvoices',
         'get_invoice'         => 'handleGetInvoice',
         'get_customer_cards'  => 'handleGetCustomerCards',
      ]);
      $this->buttons = array_merge($this->buttons, [
         'import_from_helcim' => 'Import Products from Helcim',
         'sync_to_helcim'    => 'Sync Site to Helcim',
      ]);
   }
   /**
    * Initialize service-specific settings
    */
   /*****************************************************************
    * ABSTRACT IMPLEMENTATIONS
    *****************************************************************/
   protected function initialize(): void
   {
      $this->api_token = $this->credentials['api_token'] ?? '';
      $this->account_id = $this->credentials['account_id'] ?? '';
      $this->terminal_id = $this->credentials['terminal_id'] ?? '';
      $this->webhook_secret = $this->credentials['webhook_secret'] ?? '';
      $this->is_test_mode = (bool)($this->credentials['test_mode'] ?? false);
      if (empty($this->credentials)) {
         $this->loadCredentials();
      }
      // Set the appropriate API base
      $this->apiBase = $this->is_test_mode ? $this->apiBase['sandbox'] : $this->apiBase['production'];
      // Load payment form settings
      $this->payment_form_settings = [
         'card' => $this->credentials['enable_card_payments'] ?? true,
         'ach' => $this->credentials['enable_ach_payments'] ?? false,
         'apple_pay' => $this->credentials['enable_apple_pay'] ?? false,
         'google_pay' => $this->credentials['enable_google_pay'] ?? false,
      $this->apiEndpoints = [
         'connection-test',
         'helcim-pay/initialize',
         'invoices',
         'customers',
         'payment/purchase',
         'payment/preauth',
         'payment/capture',
         'payment/refund',
         'payment/verify',
         'card-transactions',
         'card-batches',
      ];
   }
   protected function getRequestHeaders(): array
   {
      return [
         'api-token'    => $this->credentials['api_token'] ?? '',
         'Content-Type' => 'application/json',
         'Accept'       => 'application/json',
      ];
   }
   protected function performConnectionTest(): bool
   {
      try {
         $response = $this->getRequest('connection-test', [], null, 'none', true);
         return !is_wp_error($response) && !$this->isErrorResponse($response ?? []);
      } catch (Exception $e) {
         $this->logError('Connection test failed', ['error' => $e->getMessage()]);
         return false;
      }
   }
   /*****************************************************************
    * CONTENT TYPES — product field definitions
    *****************************************************************/
   protected function setContentTypes(): void
   {
      $this->has_content = true;
      $this->defaultContent = 'REGULAR';
      $types = ['REGULAR', 'SERVICE', 'DIGITAL', 'FOOD_AND_BEV', 'EVENT', 'SUBSCRIPTION', 'DONATION'];
      foreach ($types as $type) {
         $t = $type === 'REGULAR' ? null : $type;
         $this->contentTypes[$type] = $this->getHelcimMeta($t);
      }
   }
   /**
    * Register additional WordPress hooks
    * Get Helcim product meta fields by type.
    *
    * Used by FieldRegistry when 'use_helcim' => true is set
    * in a JVB_CONTENT definition.
    */
   public function getHelcimMeta(?string $type = null): array
   {
      $fields = [
         // Basic Product Fields
         'price' => [
            'type'        => 'number',
            'bulkEdit'    => true,
            'label'       => 'Price',
            'step'        => 0.01,
            'max'         => 99999,
            'description' => 'Product price'
         ],
         'product_type' => [
            'type'  => 'select',
            'label' => 'Product Type',
            'options' => [
               'REGULAR'       => 'Regular Product',
               'SERVICE'       => 'Service',
               'DIGITAL'       => 'Digital Product',
               'FOOD_AND_BEV'  => 'Food & Beverage',
               'EVENT'         => 'Event/Ticket',
               'SUBSCRIPTION'  => 'Subscription',
               'DONATION'      => 'Donation'
            ],
            'default' => $type ?? 'REGULAR'
         ],
         'cart_quantity' => [
            'type'   => 'number',
            'label'  => 'Quantity',
            'hidden' => true,
         ],
         // Tax & Shipping
         'tax_exempt' => [
            'type'    => 'true_false',
            'label'   => 'Tax Exempt',
            'section' => 'helcim-tax'
         ],
         'shipping_required' => [
            'type'    => 'true_false',
            'label'   => 'Shipping Required',
            'section' => 'helcim-shipping'
         ],
         'shipping_weight' => [
            'type'        => 'number',
            'label'       => 'Shipping Weight (kg)',
            'step'        => 0.01,
            'section'     => 'helcim-shipping',
            'condition'   => [
               'field'    => 'shipping_required',
               'operator' => '==',
               'value'    => true
            ]
         ],
         // Availability
         'available_online' => [
            'type'    => 'true_false',
            'label'   => 'Available Online',
            'section' => 'helcim-availability',
            'default' => true
         ],
         'available_for_pickup' => [
            'type'    => 'true_false',
            'label'   => 'Available for Pickup',
            'section' => 'helcim-availability',
            'default' => true
         ],
         'available_for_delivery' => [
            'type'    => 'true_false',
            'label'   => 'Available for Delivery',
            'section' => 'helcim-availability',
            'default' => false
         ],
         '_helcim_sku' => [
            'type'        => 'text',
            'label'       => 'SKU',
            'description' => 'Stock keeping unit',
            'section'     => 'helcim-config'
         ],
         // Product Variations
         'product_variations' => [
            'type'        => 'repeater',
            'label'       => 'Product Variations',
            'description' => 'Different versions of this product',
            'add_label'   => 'Add Variation',
            'section'     => 'variations',
            'fields'      => $this->getHelcimVariationMeta($type)
         ],
         // Product Options
         'options' => [
            'type'   => 'group',
            'label'  => 'Product Options',
            'section'=> 'helcim-options',
            'fields' => [
               'max_order' => [
                  'type'    => 'number',
                  'label'   => 'Maximum per order',
                  'default' => 50
               ],
               'min_order' => [
                  'type'    => 'number',
                  'label'   => 'Minimum per order',
                  'default' => 0,
               ],
               'step' => [
                  'type'    => 'number',
                  'label'   => 'Order increment',
                  'default' => 1,
               ],
               'preparation_time' => [
                  'type'        => 'number',
                  'label'       => 'Preparation time (minutes)',
                  'description' => 'Time needed to prepare this item',
                  'condition'   => [
                     'field'    => 'product_type',
                     'operator' => 'in',
                     'value'    => ['FOOD_AND_BEV', 'SERVICE']
                  ]
               ]
            ]
         ],
         // Subscription Fields
         'subscription_settings' => [
            'type'      => 'group',
            'label'     => 'Subscription Settings',
            'section'   => 'helcim-subscription',
            'condition' => [
               'field'    => 'product_type',
               'operator' => '==',
               'value'    => 'SUBSCRIPTION'
            ],
            'fields' => [
               'billing_cycle' => [
                  'type'    => 'select',
                  'label'   => 'Billing Cycle',
                  'options' => [
                     'daily'     => 'Daily',
                     'weekly'    => 'Weekly',
                     'monthly'   => 'Monthly',
                     'quarterly' => 'Quarterly',
                     'yearly'    => 'Yearly'
                  ],
                  'default' => 'monthly'
               ],
               'trial_period' => [
                  'type'        => 'number',
                  'label'       => 'Trial Period (days)',
                  'description' => 'Free trial period before billing starts',
                  'default'     => 0
               ],
               'setup_fee' => [
                  'type'        => 'number',
                  'label'       => 'Setup Fee',
                  'step'        => 0.01,
                  'description' => 'One-time setup fee'
               ]
            ]
         ],
         // Food & Beverage Specific
         'food_settings' => [
            'type'      => 'group',
            'label'     => 'Food & Beverage Settings',
            'section'   => 'helcim-food',
            'condition' => [
               'field'    => 'product_type',
               'operator' => '==',
               'value'    => 'FOOD_AND_BEV'
            ],
            'fields' => [
               'ingredients' => [
                  'type'        => 'textarea',
                  'label'       => 'Ingredients',
                  'description' => 'List ingredients (comma separated)'
               ],
               'allergens' => [
                  'type'        => 'checkbox_list',
                  'label'       => 'Allergens',
                  'options'     => [
                     'gluten'   => 'Contains Gluten',
                     'dairy'    => 'Contains Dairy',
                     'nuts'     => 'Contains Nuts',
                     'soy'      => 'Contains Soy',
                     'eggs'     => 'Contains Eggs',
                     'seafood'  => 'Contains Seafood'
                  ]
               ],
               'dietary_options' => [
                  'type'    => 'checkbox_list',
                  'label'   => 'Dietary Options',
                  'options' => [
                     'vegetarian'   => 'Vegetarian',
                     'vegan'        => 'Vegan',
                     'gluten_free'  => 'Gluten Free',
                     'dairy_free'   => 'Dairy Free',
                     'keto'         => 'Keto Friendly',
                     'halal'        => 'Halal',
                     'kosher'       => 'Kosher'
                  ]
               ],
               'spice_level' => [
                  'type'    => 'range',
                  'label'   => 'Spice Level',
                  'min'     => 0,
                  'max'     => 5,
                  'default' => 0
               ],
               'serving_size' => [
                  'type'        => 'text',
                  'label'       => 'Serving Size',
                  'description' => 'e.g., "Serves 2-3"'
               ]
            ]
         ],
         // Service Specific
         'service_settings' => [
            'type'      => 'group',
            'label'     => 'Service Settings',
            'section'   => 'helcim-service',
            'condition' => [
               'field'    => 'product_type',
               'operator' => '==',
               'value'    => 'SERVICE'
            ],
            'fields' => [
               'service_duration' => [
                  'type'        => 'number',
                  'label'       => 'Duration (minutes)',
                  'description' => 'Service duration in minutes'
               ],
               'booking_required' => [
                  'type'  => 'true_false',
                  'label' => 'Booking Required'
               ],
               'capacity' => [
                  'type'        => 'number',
                  'label'       => 'Service Capacity',
                  'description' => 'Maximum number of customers per service'
               ],
               'staff_required' => [
                  'type'        => 'number',
                  'label'       => 'Staff Required',
                  'description' => 'Number of staff needed',
                  'default'     => 1
               ]
            ]
         ],
         // Event Specific
         'event_settings' => [
            'type'      => 'group',
            'label'     => 'Event Settings',
            'section'   => 'helcim-event',
            'condition' => [
               'field'    => 'product_type',
               'operator' => '==',
               'value'    => 'EVENT'
            ],
            'fields' => [
               'event_date' => [
                  'type'  => 'datetime',
                  'label' => 'Event Date & Time'
               ],
               'event_location' => [
                  'type'  => 'text',
                  'label' => 'Event Location'
               ],
               'max_attendees' => [
                  'type'  => 'number',
                  'label' => 'Maximum Attendees'
               ],
               'early_bird_price' => [
                  'type'        => 'number',
                  'label'       => 'Early Bird Price',
                  'step'        => 0.01,
                  'description' => 'Discounted price for early registrations'
               ],
               'early_bird_deadline' => [
                  'type'  => 'date',
                  'label' => 'Early Bird Deadline'
               ]
            ]
         ]
      ];
      // Add inventory fields if configured
      if ($config['hasInventory'] ?? false) {
         $fields['_helcim_inventory'] = [
            'type'     => 'number',
            'label'    => 'Inventory',
            'bulkEdit' => true,
            'section'  => 'inventory'
         ];
         $fields['track_inventory'] = [
            'type'    => 'true_false',
            'label'   => 'Track Inventory',
            'section' => 'inventory',
            'default' => true
         ];
         $fields['low_stock_threshold'] = [
            'type'        => 'number',
            'label'       => 'Low Stock Alert',
            'description' => 'Alert when stock falls below this level',
            'section'     => 'inventory',
            'default'     => 5
         ];
         $fields['product_variations']['fields']['inventory'] = [
            'type'        => 'number',
            'label'       => 'Stock Quantity',
            'description' => 'Current stock for this variation'
         ];
      }
      return $fields;
   }
   public function getHelcimVariationMeta(?string $type = null):array
   {
      $base = [
         'name' => [
            'type'        => 'text',
            'label'       => 'Variation Name',
            'description' => 'e.g., "Small", "Large", "Red"'
         ],
         'price' => [
            'type'        => 'number',
            'label'       => 'Price',
            'step'        => 0.01,
            'max'         => 99999,
            'description' => 'Price for this variation'
         ],
         'sku' => [
            'type'        => 'text',
            'label'       => 'SKU',
            'description' => 'Stock keeping unit for this variation'
         ],
         'track_inventory' => [
            'type'  => 'true_false',
            'label' => 'Track Inventory',
         ],
         '_helcim_variation_id' => [
            'type'        => 'text',
            'label'       => 'Helcim Variation ID',
            'description' => 'Helcim ID for this variation',
            'hidden'      => true
         ],
         '_helcim_last_sync' => [
            'type'   => 'datetime',
            'label'  => 'Last Sync',
            'hidden' => true
         ],
         'options' => [
            'type'        => 'group',
            'label'       => 'Variation Options',
            'collapsible' => true,
            'fields'      => [
               'color' => [
                  'type'        => 'color',
                  'label'       => 'Color',
                  'description' => 'Visual color for this variation'
               ],
               'size' => [
                  'type'    => 'select',
                  'label'   => 'Size',
                  'options' => [
                     ''      => 'N/A',
                     'xs'    => 'Extra Small',
                     's'     => 'Small',
                     'm'     => 'Medium',
                     'l'     => 'Large',
                     'xl'    => 'Extra Large',
                     'xxl'   => '2X Large',
                     'custom'=> 'Custom'
                  ]
               ],
               'custom_size' => [
                  'type'        => 'text',
                  'label'       => 'Custom Size',
                  'condition'   => [
                     'field'    => 'size',
                     'operator' => '==',
                     'value'    => 'custom'
                  ]
               ],
               'weight' => [
                  'type'        => 'number',
                  'label'       => 'Weight (kg)',
                  'step'        => 0.01,
                  'description' => 'Weight of this variation'
               ],
               'dimensions' => [
                  'type'   => 'group',
                  'label'  => 'Dimensions',
                  'fields' => [
                     'length' => [
                        'type'  => 'number',
                        'label' => 'Length (cm)',
                        'step'  => 0.1
                     ],
                     'width' => [
                        'type'  => 'number',
                        'label' => 'Width (cm)',
                        'step'  => 0.1
                     ],
                     'height' => [
                        'type'  => 'number',
                        'label' => 'Height (cm)',
                        'step'  => 0.1
                     ]
                  ]
               ]
            ]
         ]
      ];
      $extras = [
         'SERVICE' => [
            'service_duration'   => [
               'type'        => 'number',
               'label'       => 'Duration (minutes)',
               'description' => 'Duration for this service variation'
            ],
            'available_for_booking' => [
               'type'  => 'true_false',
               'label' => 'Available for Booking'
            ]
         ],
         'FOOD_AND_BEV' => [
            'portion_size' => [
               'type'    => 'select',
               'label'   => 'Portion Size',
               'options' => [
                  'small'    => 'Small',
                  'regular'  => 'Regular',
                  'large'    => 'Large',
                  'family'   => 'Family Size'
               ]
            ],
            'calories'  => [
               'type'        => 'number',
               'label'       => 'Calories',
               'description' => 'Calorie count for this variation'
            ]
         ],
         'DIGITAL'   => [
            'download_limit'  => [
               'type'        => 'number',
               'label'       => 'Download Limit',
               'description' => 'Maximum number of downloads',
               'default'     => -1
            ],
            'expiry_days'  => [
               'type'        => 'number',
               'label'       => 'Access Duration (days)',
               'description' => 'Days until download expires',
               'default'     => 0
            ]
         ]
      ];
      if ($type && array_key_exists($type, $extras)){
         $base = array_merge($base, $extras[$type]);
      }
      return $base;
   }
   /*****************************************************************
    * HELCIMPAY.JS — Frontend Scripts & Checkout Initialization
    *****************************************************************/
   protected function registerAdditionalHooks(): void
   {
      $this->ensureInitialized();
      if (!$this->isSetUp()) {
         return;
      }
      // User login tracking for security
      add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
      add_action('wp_footer', [$this, 'outputCheckout']);
      // Enqueue checkout scripts
      add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
      // REST API endpoints for checkout
      add_action('rest_api_init', [$this, 'registerRestRoutes']);
   }
      // Shared checkout UI (replaces provider-specific outputCheckout)
      add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
   /**
    * Register REST API routes
    */
   public function registerRestRoutes(): void
   {
      register_rest_route('jvb/v1', '/helcim/checkout', [
         'methods' => 'POST',
         'callback' => [$this, 'handleCheckout'],
         'permission_callback' => '__return_true'
      ]);
      register_rest_route('jvb/v1', '/helcim/customer', [
         'methods' => 'POST',
         'callback' => [$this, 'handleCustomerLookup'],
         'permission_callback' => '__return_true'
      ]);
      register_rest_route('jvb/v1', '/helcim/order-status/(?P<order_id>[a-zA-Z0-9-]+)', [
         'methods' => 'GET',
         'callback' => [$this, 'handleOrderStatus'],
         'permission_callback' => '__return_true'
      ]);
      register_rest_route('jvb/v1', '/helcim/create-account', [
         'methods' => 'POST',
         'callback' => [$this, 'handleAccountCreation'],
         'permission_callback' => '__return_true'
      ]);
   }
   /**
    * Initialize field mappings for all content types
    */
   private function initializeFieldMappings(): void
   {
      foreach (JVB_CONTENT as $key => $config) {
         if (isset($config['integrations']['helcim'])) {
            $post_type = jvbCheckBase($key);
            $this->field_mappings[$post_type] = $this->getFieldMapping($post_type);
      // Checkout description filter
      add_filter('jvb_checkout_description', function (string $desc, string $provider) {
         if ($provider === 'helcim') {
            return 'Securely checkout with your name, email, and payments processed by Helcim.';
         }
      }
         return $desc;
      }, 10, 2);
      // Register queue operation types with IntegrationExecutor
      $this->registerQueueTypes();
      // Register webhook endpoint (handled by parent)
      $this->registerWebhookEndpoint();
   }
   /**
    * Get field mapping for a post type
    */
   public function getFieldMapping(string $post_type): array
   {
      return apply_filters(BASE . '_helcim_field_mapping', [
         'name' => 'title',
         'description' => 'content',
         'price' => 'price',
         'sku' => '_helcim_sku',
         'product_code' => '_helcim_product_code',
         'inventory' => '_helcim_inventory',
         'product_type' => 'product_type',
         'tax_exempt' => 'tax_exempt',
         'shipping_required' => 'shipping_required'
      ], $post_type);
   }
   /**
    * Output checkout form
    */
   public function outputCheckout(): void
   {
      if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
         return;
      }
      ?>
      <button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
         <?= jvbIcon('shopping-cart')?><span class="abs"></span><span class="abs count"></span>
      </button>
      <aside id="cart" class="main">
         <form id="checkout" data-form-id="checkout" data-save="checkout">
            <?php
            $tabs = [
               'cartItems' => [
                  'title'  => 'Your Order',
                  'icon'   => 'cart',
                  'description' => 'Review and modify your order items',
                  'content'   => $this->cartContent()
               ],
               'account' => [
                  'title' => 'Account',
                  'icon' => 'user',
                  'description' => $this->getAccountTabDescription(),
                  'content' => $this->renderAccountSection()
               ],
               'checkout'  => [
                  'title'  => 'Checkout',
                  'icon'   => 'checkout',
                  'description' => 'Complete your order with Helcim secure payments',
                  'content'   => $this->renderCheckoutSection()
               ],
               'order'  => [
                  'title'  => 'Order Status',
                  'icon' => 'truck',
                  'hidden' => true,
                  'description' => 'Track your order status',
                  'content'   => $this->renderOrderStatus()
               ]
            ];
            jvbRenderTabs($tabs);
            ?>
            <div class="cart-total row end">
               <p class="tax">Tax: <span></span></p>
               <p class="total">GRAND TOTAL: <span></span></p>
            </div>
         </form>
      </aside>
      <?php
      $this->outputCheckoutTemplates();
   }
   /**
    * Get account tab description based on login status
    */
   private function getAccountTabDescription(): string
   {
      if (is_user_logged_in()) {
         return 'Manage your account and view order history';
      }
      return 'Login or create an account for faster checkout';
   }
   /**
    * Render account section
    */
   private function renderAccountSection(): string
   {
      ob_start();
      ?>
      <div class="account-section">
         <?php if (is_user_logged_in()): ?>
            <?php $this->renderLoggedInAccount(); ?>
         <?php else: ?>
            <?php $this->renderGuestAccount(); ?>
         <?php endif; ?>
      </div>
      <?php
      return ob_get_clean();
   }
   /**
    * Render logged in account view
    */
   private function renderLoggedInAccount(): void
   {
      $user = wp_get_current_user();
      $customer_id = get_user_meta($user->ID, BASE . '_helcim_customer_id', true);
      ?>
      <div class="logged-in-account">
         <p>Welcome back, <?= esc_html($user->display_name) ?>!</p>
         <div class="account-actions">
            <button type="button" class="button" onclick="helcimCheckout.loadSavedCards()">
               <?= jvbIcon('credit-card') ?> Saved Cards
            </button>
            <button type="button" class="button" onclick="helcimCheckout.loadOrderHistory()">
               <?= jvbIcon('receipt') ?> Order History
            </button>
            <button type="button" class="button" onclick="helcimCheckout.loadFavorites()">
               <?= jvbIcon('heart') ?> Favorites
            </button>
         </div>
         <div id="account-content"></div>
      </div>
      <?php
   }
   /**
    * Render guest account view
    */
   private function renderGuestAccount(): void
   {
      ?>
      <div class="guest-account">
         <div class="login-section">
            <h3>Returning Customer?</h3>
            <p>Login with your email to access saved cards and order history</p>
            <div class="email-login">
               <input type="email"
                     id="login-email"
                     placeholder="Enter your email"
                     autocomplete="email">
               <button type="button"
                     class="button primary"
                     onclick="helcimCheckout.loginWithEmail()">
                  Continue
               </button>
            </div>
            <div id="login-status"></div>
         </div>
         <div class="guest-checkout">
            <h3>New Customer?</h3>
            <p>You can checkout as a guest or create an account after your order</p>
            <label>
               <input type="checkbox" id="create-account-offer">
               Offer to create account after checkout
            </label>
         </div>
      </div>
      <?php
   }
   /**
    * Render checkout section
    */
   private function renderCheckoutSection(): string
   {
      ob_start();
      ?>
      <div class="checkout-section">
         <h3>Customer Information</h3>
         <input type="text" name="name" placeholder="Full Name" required autocomplete="name">
         <input type="email" name="email" placeholder="Email" required autocomplete="email">
         <input type="tel" name="phone" placeholder="Phone" required autocomplete="tel">
         <h3>Pickup/Delivery Details</h3>
         <select name="fulfillment_type" required>
            <option value="pickup">Pickup</option>
            <option value="delivery">Delivery</option>
         </select>
         <div class="pickup-details" data-show-if="fulfillment_type:pickup">
            <input type="datetime-local" name="pickup_time" required>
         </div>
         <div class="delivery-details" data-show-if="fulfillment_type:delivery" style="display:none;">
            <input type="text" name="delivery_address" placeholder="Delivery Address" autocomplete="street-address">
            <input type="text" name="delivery_instructions" placeholder="Delivery Instructions">
         </div>
         <textarea name="special_instructions" placeholder="Special instructions or dietary notes"></textarea>
         <h3>Payment Information</h3>
         <div id="saved-cards"></div>
         <div id="helcim-card-container"></div>
         <button type="submit" class="button primary checkout-button">
            Place Order
         </button>
      </div>
      <?php
      return ob_get_clean();
   }
   /**
    * Render order status section
    */
   protected function renderOrderStatus(): string
   {
      ob_start();
      ?>
      <div class="order-confirmation">
         <h2>Order Confirmed!</h2>
         <div id="order-status" data-order="">
            <p>Order #<span class="order-num"></span></p>
            <div class="status-timeline">
               <div class="status-item active" data-status="received">Order Received</div>
               <div class="status-item" data-status="preparing">Preparing</div>
               <div class="status-item" data-status="ready">Ready</div>
               <div class="status-item" data-status="complete">Complete</div>
            </div>
            <div class="order-eta">
               Estimated time: <span id="eta">Calculating...</span>
            </div>
         </div>
      </div>
      <?php
      return ob_get_clean();
   }
   /**
    * Output checkout templates
    */
   private function outputCheckoutTemplates(): void
   {
      ?>
      <template class="cartItem">
         <tr class="item">
            <td class="item">
               <label for="quantity"></label>
               <div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="" data-id="">
                  <button type="button" class="decrease" aria-label="Decrease quantity">
                     <?= jvbIcon('minus-square') ?>
                  </button>
                  <input type="number" name="quantity" value="1" min="0" max="50">
                  <button type="button" class="increase" aria-label="Increase quantity">
                     <?= jvbIcon('plus') ?>
                  </button>
               </div>
            </td>
            <td class="price"></td>
            <td class="total"></td>
            <td>
               <button type="button" class="remove" aria-label="Remove item">
                  <?= jvbIcon('trash') ?>
               </button>
            </td>
         </tr>
      </template>
      <template class="savedCard">
         <label class="saved-card-option">
            <input type="radio" name="payment_method" value="">
            <span class="card-details">
               <span class="card-brand"></span>
               •••• <span class="last-4"></span>
               <span class="exp-date"></span>
            </span>
         </label>
      </template>
      <?php
   }
   /**
    * Cart content section
    */
   private function cartContent(): string
   {
      ob_start();
      ?>
      <div class="cart-items">
         <table>
            <thead>
            <tr>
               <th>Item</th>
               <th>Price</th>
               <th>Total</th>
               <th></th>
            </tr>
            </thead>
            <tbody></tbody>
         </table>
         <div class="cart-actions">
            <button type="button" class="button" onclick="helcimCheckout.clearCart()">
               <?= jvbIcon('trash') ?> Clear Cart
            </button>
         </div>
      </div>
      <?php
      return ob_get_clean();
   }
   /**
    * Enqueue checkout scripts
    */
   public function enqueueScripts(): void
   {
      $this->ensureInitialized();
      if (!$this->isSetUp()) {
         return;
      }
      // Helcim JS SDK
      $sdk_url = $this->is_test_mode
         ? 'https://helcim-js-sandbox.helcim.com/v1/helcim.js'
         : 'https://js.helcim.com/v1/helcim.js';
      // HelcimPay.js SDK
      wp_enqueue_script(
         'helcim-js-sdk',
         $sdk_url,
         'helcim-pay-sdk',
         'https://secure.helcim.app/helcim-pay/services/start.js',
         [],
         null,
         [
            'strategy' => 'defer',
            'in_footer' => true
         ]
         true
      );
      // Register custom checkout script
      // Base cart checkout (shared with Square)
      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'],
         '1.1.31',
         true
      );
      // Helcim checkout (extends CartCheckout)
      wp_register_script(
         'jvb-helcim-checkout',
         JVB_URL . 'assets/js/min/helcim.min.js',
         [
            'jvb-utility',
            'jvb-queue',
            'jvb-a11y',
            'jvb-cache',
            'jvb-tabs',
            'jvb-modal',
         ],
         '1.0.0',
         [
            'strategy' => 'defer',
            'in_footer' => true
         ]
         ['jvb-checkout', 'helcim-pay-sdk'],
         '1.1.31',
         true
      );
      wp_localize_script('jvb-helcim-checkout', 'helcimConfig', [
         'api_url'      => rest_url('jvb/v1/helcim/'),
         'nonce'        => wp_create_nonce('wp_rest'),
         'currency'     => $this->credentials['currency'] ?? 'CAD',
         'is_logged_in' => is_user_logged_in(),
         'user_email'   => is_user_logged_in() ? wp_get_current_user()->user_email : '',
         'isOpen'       => apply_filters('jvb_store_is_open', '1'),
      ]);
      wp_enqueue_script('jvb-helcim-checkout');
      // Localize the checkout script with Helcim config
      wp_localize_script(
         'jvb-helcim-checkout',
         'helcimConfig',
         [
            'isOpen' => jvbIsOpen(),
            'apiUrl' => rest_url('jvb/v1/helcim/'),
            'nonce' => wp_create_nonce('wp_rest'),
            'accountId' => $this->account_id,
            'testMode' => $this->is_test_mode
         ]
      );
   }
   /******************************************************************
    * POST SYNC METHODS
    ******************************************************************/
   protected function registerQueueTypes(): void
   {
      $queue    = JVB()->queue();
      $executor = new IntegrationExecutor();
      $queue->registry()->register('helcim_sync_to', new TypeConfig(
         executor:  $executor,
         chunkKey:  'items',
         chunkSize: 10,
         maxRetries: 3
      ));
      $queue->registry()->register('helcim_sync_from', new TypeConfig(
         executor:  $executor,
         chunkKey:  'items',
         chunkSize: 10,
         maxRetries: 3
      ));
      $queue->registry()->register('helcim_delete_from', new TypeConfig(
         executor:  $executor,
         chunkKey:  'external_ids',
         chunkSize: 20,
         maxRetries: 2
      ));
      $queue->registry()->register('helcim_import', new TypeConfig(
         executor:   $executor,
         maxRetries: 3
      ));
      $queue->registry()->register('helcim_sync_customer', new TypeConfig(
         executor:   $executor,
         maxRetries: 2
      ));
   }
   /**
    * Handle post save for Helcim sync
    * Initialize a HelcimPay.js checkout session.
    *
    * Server-side: POST /helcim-pay/initialize → returns checkoutToken + secretToken.
    * Client-side: appendHelcimPayIframe(checkoutToken) renders the payment modal.
    * Tokens are valid for 60 minutes.
    *
    * @param array $data [
    *     'amount'      => float,   // Required
    *     'invoiceId'   => string,  // Optional — pay a specific invoice
    *     'customerId'  => int,     // Optional — associate with Helcim customer
    *     'paymentType' => string,  // purchase|preauth|verify (default: purchase)
    * ]
    */
   public function initializeCheckout(array $data): array
   {
      if (empty($data['amount']) || (float) $data['amount'] <= 0) {
         return ['success' => false, 'message' => 'Invalid amount'];
      }
      $paymentMethod = !empty($this->credentials['allow_ach']) ? 'cc-ach' : 'cc';
      $body = [
         'paymentType'   => $data['paymentType'] ?? 'purchase',
         'amount'        => (float) $data['amount'],
         'currency'      => $this->credentials['currency'] ?? 'CAD',
         'paymentMethod' => $paymentMethod,
      ];
      if (!empty($data['invoiceId'])) {
         $body['invoiceNumber'] = $data['invoiceId'];
      }
      if (!empty($data['customerId'])) {
         $body['customerId'] = (int) $data['customerId'];
      }
      if (!empty($this->credentials['fee_saver'])) {
         $body['hasConvenienceFee'] = true;
      }
      $response = $this->postRequest('helcim-pay/initialize', $body);
      if (is_wp_error($response)) {
         return ['success' => false, 'message' => $response->get_error_message()];
      }
      if (empty($response['checkoutToken'])) {
         return ['success' => false, 'message' => 'Failed to initialize checkout'];
      }
      return [
         'success'       => true,
         'checkoutToken' => $response['checkoutToken'],
         'secretToken'   => $response['secretToken'] ?? '',
      ];
   }
   /*****************************************************************
    * INVOICES — Helcim is source of truth
    *****************************************************************/
   /**
    * Get invoices for a customer.
    *
    * @param array $data ['email' => string] or ['customerId' => int]
    */
   public function handleGetInvoices(array $data): array
   {
      $customerId = $data['customerId'] ?? null;
      if (empty($customerId) && !empty($data['email'])) {
         $customerId = $this->getCustomerIdByEmail($data['email']);
      }
      if (!$customerId) {
         return ['success' => true, 'invoices' => []];
      }
      $response = $this->getRequest('invoices', ['customerId' => $customerId], null, 'minimal');
      if (is_wp_error($response) || !is_array($response)) {
         return ['success' => false, 'message' => 'Failed to fetch invoices'];
      }
      return ['success' => true, 'invoices' => $response];
   }
   /**
    * Get a single invoice by ID.
    */
   public function handleGetInvoice(array $data): array
   {
      $invoiceId = $data['invoiceId'] ?? null;
      if (!$invoiceId) {
         return ['success' => false, 'message' => 'Invoice ID required'];
      }
      $response = $this->getRequest("invoices/{$invoiceId}", [], null, 'minimal');
      if (is_wp_error($response) || !is_array($response)) {
         return ['success' => false, 'message' => 'Failed to fetch invoice'];
      }
      return ['success' => true, 'invoice' => $response];
   }
   /*****************************************************************
    * CUSTOMERS & CARDS
    *****************************************************************/
   /**
    * Find Helcim customer ID by email.
    */
   public function getCustomerIdByEmail(string $email): ?int
   {
      $cacheKey = 'customer_email_' . md5($email);
      $cached   = $this->cache->get($cacheKey);
      if ($cached !== false) {
         return (int) $cached;
      }
      $response = $this->getRequest('customers', ['search' => $email], null, 'none', true);
      if (is_wp_error($response) || empty($response)) {
         return null;
      }
      $customers = is_array($response) ? $response : [];
      $emailLower = strtolower($email);
      foreach ($customers as $customer) {
         $contactEmail = strtolower($customer['contactEmail'] ?? $customer['email'] ?? '');
         if ($contactEmail === $emailLower) {
            $this->cache->set($cacheKey, $customer['id'], $this->cacheStrategy['aggressive']);
            return (int) $customer['id'];
         }
      }
      return null;
   }
   /**
    * Get or create a Helcim customer.
    */
   public function getOrCreateCustomer(array $info): ?int
   {
      if (empty($info['email'])) {
         return null;
      }
      $existing = $this->getCustomerIdByEmail($info['email']);
      if ($existing) {
         return $existing;
      }
      $response = $this->postRequest('customers', [
         'contactName'  => $info['name'] ?? '',
         'contactEmail' => $info['email'],
         'cellphone'    => $info['phone'] ?? '',
      ]);
      if (is_wp_error($response) || empty($response['id'])) {
         return null;
      }
      return (int) $response['id'];
   }
   /**
    * Get saved cards for a customer.
    */
   public function handleGetCustomerCards(array $data): array
   {
      $customerId = $data['customerId'] ?? null;
      if (empty($customerId) && !empty($data['email'])) {
         $customerId = $this->getCustomerIdByEmail($data['email']);
      }
      if (!$customerId) {
         return ['success' => true, 'cards' => []];
      }
      $response = $this->getRequest("customers/{$customerId}/cards", [], null, 'moderate');
      if (is_wp_error($response) || !is_array($response)) {
         return ['success' => false, 'message' => 'Failed to fetch cards'];
      }
      return ['success' => true, 'cards' => $response];
   }
   /**
    * Get bank accounts for a customer.
    */
   public function getCustomerBankAccounts(int $customerId): array
   {
      $response = $this->getRequest("customers/{$customerId}/bank-accounts", [], null, 'moderate');
      return (!is_wp_error($response) && is_array($response)) ? $response : [];
   }
   /*****************************************************************
    * TRANSACTIONS
    *****************************************************************/
   public function getTransactions(array $params = []): array
   {
      $response = $this->getRequest('card-transactions', $params, null, 'minimal');
      return (!is_wp_error($response) && is_array($response)) ? $response : [];
   }
   public function refundPayment(array $data): array
   {
      $response = $this->postRequest('payment/refund', $data);
      if (is_wp_error($response)) {
         return ['success' => false, 'message' => $response->get_error_message()];
      }
      return ['success' => true, 'transaction' => $response];
   }
   /*****************************************************************
    * PRODUCT SYNC
    *****************************************************************/
   protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
   {
      // Queue the sync operation
      $this->queueOperation('sync_to_helcim', [
         'items' => [$postID],
      $fields = $this->getSyncFields($postID, 'post', ['share_to_helcim', 'schedule_helcim']);
      if (empty($fields['share_to_helcim'])) {
         return;
      }
      // Uses IntegrationExecutor via TypeRegistry instead of FilteredExecutor
      $this->queueOperation('sync_to', [
         'items'   => [$postID],
         'user_id' => $this->userID,
         'content_type' => $settings['content_type'] ?? 'REGULAR'
      ], [
         'priority' => 'high',
         'delay' => 30, // Small delay to batch multiple saves
         'delay'    => 30,
      ]);
      update_post_meta($postID, BASE . '_helcim_sync_status', 'queued');
   }
   /**
    * Handle post deletion
    */
   public function handleDeletePost(int $postID): void
   protected function handleImportFromHelcim(): array
   {
      $helcim_id = get_post_meta($postID, BASE . '_helcim_product_id', true);
      $this->queueOperation('import_products', [
         'user_id' => $this->userID,
      ], ['priority' => 'normal']);
      if ($helcim_id) {
         $this->queueOperation('delete_from_helcim', [
            'helcim_ids' => [$helcim_id],
            'post_id' => $postID
         ], [
            'priority' => 'high'
         ]);
      }
      return ['success' => true, 'message' => 'Import from Helcim queued'];
   }
   /*****************************************************************
    * USER ↔ CUSTOMER LINKING
    *****************************************************************/
   public function linkUserToCustomer(int $userId, int $helcimCustomerId): void
   {
      update_user_meta($userId, BASE . '_helcim_customer_id', $helcimCustomerId);
   }
   public function getUserCustomerId(int $userId): ?int
   {
      $id = get_user_meta($userId, BASE . '_helcim_customer_id', true);
      return $id ? (int) $id : null;
   }
   /**
    * Process queued operations
    * Resolve customer ID from user meta, falling back to email lookup + auto-link.
    */
   public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
   public function resolveCustomerId(int $userId): ?int
   {
      $base = strtolower($this->service_name).'_';
      $helcim = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
      switch ($operation->type) {
         case $base.'sync_to_helcim':
            return $helcim->processSyncToHelcim($data);
         case $base.'delete_from_helcim':
            return $helcim->processDeleteFromHelcim($data);
         case $base.'import_catalog':
            return $helcim->processImportCatalog($data);
         case $base.'sync_customer':
            return $helcim->processSyncCustomer($data);
         default:
            return $result;
      }
   }
   /**
    * Process sync to Helcim
    */
   private function processSyncToHelcim(array $data): array
   {
      $items = $data['items'] ?? [];
      $content_type = $data['content_type'] ?? 'REGULAR';
      $success_count = 0;
      $errors = [];
      foreach ($items as $post_id) {
         try {
            $post = get_post($post_id);
            if (!$post) continue;
            $meta = Meta::forPost($post_id);
            $field_map = $this->field_mappings[$post->post_type] ?? [];
            // Prepare product data for Helcim
            $product_data = [
               'name' => $post->post_title,
               'description' => $post->post_content,
               'productCode' => get_post_meta($post_id, BASE . '_helcim_product_code', true) ?: 'WP-' . $post_id,
               'type' => $content_type,
               'price' => floatval($meta->get('price')) * 100, // Convert to cents
               'taxable' => (bool)$meta->get('is_taxable'),
            ];
            // Handle variations
            $variations = $meta->get('product_variations');
            if (!empty($variations)) {
               $product_data['variations'] = $this->prepareVariations($variations);
            }
            // Check if product exists
            $helcim_id = get_post_meta($post_id, BASE . '_helcim_product_id', true);
            if ($helcim_id) {
               // Update existing product
               $response = $this->putRequest('inventory/product/' . $helcim_id, $product_data);
            } else {
               // Create new product
               $response = $this->postRequest('inventory/product', $product_data);
               if (!is_wp_error($response) && isset($response['productId'])) {
                  update_post_meta($post_id, BASE . '_helcim_product_id', $response['productId']);
                  $helcim_id = $response['productId'];
               }
            }
            if (!is_wp_error($response)) {
               update_post_meta($post_id, BASE . '_helcim_sync_status', 'success');
               update_post_meta($post_id, BASE . '_helcim_last_sync', current_time('mysql'));
               $success_count++;
            } else {
               throw new Exception($response->get_error_message());
            }
         } catch (Exception $e) {
            $errors[] = "Post $post_id: " . $e->getMessage();
            update_post_meta($post_id, BASE . '_helcim_sync_status', 'failed');
            update_post_meta($post_id, BASE . '_helcim_sync_error', $e->getMessage());
         }
      $id = $this->getUserCustomerId($userId);
      if ($id) {
         return $id;
      }
      return [
         'success' => count($errors) === 0,
         'result' => [
            'synced' => $success_count,
            'errors' => $errors
         ]
      ];
   }
   /**
    * Prepare variations for Helcim
    */
   private function prepareVariations(array $variations): array
   {
      $helcim_variations = [];
      foreach ($variations as $index => $variation) {
         $helcim_variations[] = [
            'name' => $variation['name'] ?? '',
            'price' => floatval($variation['price'] ?? 0) * 100,
            'sku' => $variation['sku'] ?? '',
            'inventory' => intval($variation['inventory'] ?? 0),
         ];
      }
      return $helcim_variations;
   }
   /**
    * Process delete from Helcim
    */
   private function processDeleteFromHelcim(array $data): array
   {
      $helcim_ids = $data['helcim_ids'] ?? [];
      $success_count = 0;
      foreach ($helcim_ids as $helcim_id) {
         $response = $this->deleteRequest('inventory/product/' . $helcim_id);
         if (!is_wp_error($response)) {
            $success_count++;
         }
      }
      return [
         'success' => $success_count > 0,
         'result' => ['deleted' => $success_count]
      ];
   }
   /**
    * Process import from Helcim catalog
    */
   private function processImportCatalog(array $data): array
   {
      $page = 1;
      $imported = 0;
      do {
         $response = $this->getRequest('inventory/product', [
            'page' => $page,
            'limit' => 100
         ]);
         if (is_wp_error($response)) {
            break;
         }
         $products = $response['products'] ?? [];
         foreach ($products as $product) {
            $this->importHelcimProduct($product);
            $imported++;
         }
         $page++;
         $has_more = count($products) === 100;
      } while ($has_more);
      return [
         'success' => true,
         'result' => ['imported' => $imported]
      ];
   }
   /**
    * Import a single Helcim product
    */
   private function importHelcimProduct(array $product): void
   {
      // Find existing post by Helcim ID
      $args = [
         'post_type' => $this->syncPostTypes,
         'meta_key' => BASE . '_helcim_product_id',
         'meta_value' => $product['productId'],
         'posts_per_page' => 1
      ];
      $existing = get_posts($args);
      if ($existing) {
         $post_id = $existing[0]->ID;
         // Update existing post
         wp_update_post([
            'ID' => $post_id,
            'post_title' => $product['name'],
            'post_content' => $product['description'] ?? ''
         ]);
      } else {
         // Create new post
         $post_id = wp_insert_post([
            'post_title' => $product['name'],
            'post_content' => $product['description'] ?? '',
            'post_type' => $this->syncPostTypes[0] ?? 'post',
            'post_status' => 'publish'
         ]);
      }
      if ($post_id) {
         // Update meta data
         $meta = Meta::forPost($post_id);
         $meta->setAll([
            'price' => $product['price'] / 100, // Convert from cents
            '_helcim_product_id' => $product['productId'],
            '_helcim_product_code' => $product['productCode'],
            '_helcim_last_sync' => current_time('mysql')
         ]);
      }
   }
   /******************************************************************
    * CUSTOMER MANAGEMENT
    ******************************************************************/
   /**
    * Track user login for security
    */
   public function trackUserLogin(string $user_login, \WP_User $user): void
   {
      // Check if user has Helcim integration
      $user_roles = $user->roles;
      foreach ($user_roles as $role) {
         $role_key = jvbNoBase($role);
         if (isset(JVB_USER[$role_key]['integrations']['helcim']['is_customer'])) {
            $login_count = (int)get_user_meta($user->ID, BASE . '_helcim_login_count', true);
            $login_count++;
            update_user_meta($user->ID, BASE . '_helcim_login_count', $login_count);
            update_user_meta($user->ID, BASE . '_helcim_last_login', current_time('mysql'));
            // Check if password reset is needed
            if ($login_count % self::PASSWORD_RESET_INTERVAL === 0) {
               $this->schedulePasswordReset($user->ID);
            }
            break;
         }
      }
   }
   /**
    * Schedule password reset for security
    */
   private function schedulePasswordReset(int $user_id): void
   {
      update_user_meta($user_id, BASE . '_helcim_password_reset_required', true);
      // Send notification
      $user = get_user_by('ID', $user_id);
      if ($user) {
         JVB()->email()->sendEmail(
            $user->user_email,
            'Security: Password Reset Required',
            'For your security, please reset your password to continue accessing your account and saved payment methods.',
         );
      }
   }
   /**
    * Handle customer lookup
    */
   public function handleCustomerLookup(WP_REST_Request $request): WP_REST_Response
   {
      $email = sanitize_email($request->get_param('email'));
      if (!$email) {
         return new WP_REST_Response(['error' => 'Email required'], 400);
      }
      // Check WordPress user first
      $user = get_user_by('email', $email);
      if ($user) {
         // Check if user has customer role
         $has_customer_role = false;
         foreach ($user->roles as $role) {
            $role_key = jvbNoBase($role);
            if (isset(JVB_USER[$role_key]['integrations']['helcim']['is_customer'])) {
               $has_customer_role = true;
               break;
            }
         }
         if ($has_customer_role) {
            // Get saved cards and order history
            $customer_id = get_user_meta($user->ID, BASE . '_helcim_customer_id', true);
            if ($customer_id) {
               $customer_data = $this->getHelcimCustomer($customer_id);
               return new WP_REST_Response([
                  'exists' => true,
                  'has_account' => true,
                  'customer' => [
                     'name' => $user->display_name,
                     'email' => $user->user_email
                  ],
                  'cards' => $customer_data['cards'] ?? [],
                  'orders' => $this->getUserOrders($user->ID)
               ]);
            }
         }
         return new WP_REST_Response([
            'exists' => true,
            'has_account' => true,
            'no_customer_role' => true,
            'message' => 'Account exists but not set up for orders. Would you like to enable ordering?'
         ]);
      }
      // Check Helcim for customer
      $helcim_customer = $this->searchHelcimCustomer($email);
      if ($helcim_customer) {
         return new WP_REST_Response([
            'exists' => true,
            'has_account' => false,
            'helcim_only' => true,
            'message' => 'Found your previous orders. Create an account to access them?'
         ]);
      }
      return new WP_REST_Response([
         'exists' => false,
         'message' => 'New customer'
      ]);
   }
   /**
    * Get Helcim customer data
    */
   private function getHelcimCustomer(string $customer_id): array
   {
      $cached = $this->cache->get('helcim_customer_' . $customer_id);
      if ($cached !== false) {
         return $cached;
      }
      $response = $this->getRequest('customer/' . $customer_id);
      if (is_wp_error($response)) {
         return [];
      }
      // Get saved cards
      $cards_response = $this->getRequest('customer/' . $customer_id . '/cards');
      $cards = [];
      if (!is_wp_error($cards_response) && isset($cards_response['cards'])) {
         foreach ($cards_response['cards'] as $card) {
            $cards[] = [
               'id' => $card['cardToken'],
               'last_4' => $card['cardLast4'],
               'card_brand' => $card['cardBrand'],
               'exp_month' => $card['expiryMonth'],
               'exp_year' => $card['expiryYear']
            ];
         }
      }
      $customer_data = [
         'customer' => $response,
         'cards' => $cards
      ];
      $this->cache->set('helcim_customer_' . $customer_id, $customer_data, HOUR_IN_SECONDS);
      return $customer_data;
   }
   /**
    * Search for Helcim customer by email
    */
   private function searchHelcimCustomer(string $email): ?array
   {
      $response = $this->getRequest('customer/search', [
         'email' => $email
      ]);
      if (!is_wp_error($response) && isset($response['customers'][0])) {
         return $response['customers'][0];
      }
      return null;
   }
   /**
    * Get user's order history
    */
   private function getUserOrders(int $user_id): array
   {
      $orders = get_user_meta($user_id, BASE . '_helcim_orders', true) ?: [];
      // Get last 10 orders
      return array_slice($orders, -10);
   }
   /**
    * Handle account creation
    */
   public function handleAccountCreation(WP_REST_Request $request): WP_REST_Response
   {
      $email = sanitize_email($request->get_param('email'));
      $name = sanitize_text_field($request->get_param('name'));
      if (!$email || !is_email($email)) {
         return new WP_REST_Response(['error' => 'Valid email required'], 400);
      }
      // Check if user already exists
      if (email_exists($email)) {
         return new WP_REST_Response([
            'success' => false,
            'exists' => true,
            'message' => 'An account with this email already exists. Please log in instead.'
         ], 409);
      }
      // Generate username from email
      $username = sanitize_user(current(explode('@', $email)));
      $username = $this->generateUniqueUsername($username);
      // Create user account
      $user_id = wp_create_user(
         $username,
         wp_generate_password(20, true, true), // Temporary 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_REST_Response(['error' => 'Failed to create account'], 500);
      }
      // Set user role
      $user = new \WP_User($user_id);
      $user->set_role(BASE.'foodie'); // Or appropriate role from JVB_USER
      // Update display name
      if ($name) {
         wp_update_user([
            'ID' => $user_id,
            'display_name' => $name
         ]);
      }
      // Generate password reset key
      $reset_key = get_password_reset_key($user);
      if (!is_wp_error($reset_key)) {
         $this->sendWelcomeEmail($user, $reset_key);
      }
      // Link to Helcim customer if exists
      $helcim_customer = $this->searchHelcimCustomer($email);
      if ($helcim_customer) {
         update_user_meta($user_id, BASE . '_helcim_customer_id', $helcim_customer['customerId']);
      } else {
         // Create new Helcim customer
         $customer_response = $this->postRequest('customer', [
            'customerCode' => 'WP-' . $user_id,
            'contactName' => $name ?: $username,
            'email' => $email
         ]);
         if (!is_wp_error($customer_response) && isset($customer_response['customerId'])) {
            update_user_meta($user_id, BASE . '_helcim_customer_id', $customer_response['customerId']);
         }
      }
      return new WP_REST_Response([
         'success' => true,
         'message' => 'Account created! Check your email to set your password.',
         'user_id' => $user_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
    */
   private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
   {
      $site_name = get_bloginfo('name');
      $reset_url = get_home_url(null, "login?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
      );
   }
   /******************************************************************
    * ORDER PROCESSING
    ******************************************************************/
   /**
    * Handle checkout
    */
   public function handleCheckout(WP_REST_Request $request): WP_REST_Response
   {
      $cart_items = $request->get_param('items');
      $customer_info = $request->get_param('customer');
      $payment_token = $request->get_param('payment_token');
      if (empty($cart_items) || empty($payment_token)) {
         return new WP_REST_Response(['error' => 'Invalid order data'], 400);
      }
      // Calculate order total
      $order_total = $this->calculateOrderTotal($cart_items);
      // Create Helcim invoice
      $invoice_response = $this->createHelcimInvoice($cart_items, $customer_info, $order_total);
      if (is_wp_error($invoice_response)) {
         return new WP_REST_Response(['error' => $invoice_response->get_error_message()], 500);
      }
      // Process payment
      $payment_response = $this->processHelcimPayment($payment_token, $invoice_response['invoiceId'], $order_total);
      if (is_wp_error($payment_response)) {
         return new WP_REST_Response(['error' => $payment_response->get_error_message()], 500);
      }
      // Save order to user if logged in
      if (is_user_logged_in()) {
         $this->saveOrderToUser(get_current_user_id(), $invoice_response['invoiceId']);
      }
      return new WP_REST_Response([
         'success' => true,
         'order_id' => $invoice_response['invoiceId'],
         'receipt_url' => $payment_response['receiptUrl'] ?? '',
         'message' => 'Order placed successfully!'
      ]);
   }
   /**
    * Calculate order total
    */
   private function calculateOrderTotal(array $cart_items): int
   {
      $total = 0;
      foreach ($cart_items as $item) {
         $post_id = intval($item['id'] ?? 0);
         if (!$post_id) continue;
         $meta = Meta::forPost($post_id);
         $price = floatval($meta->get('price'));
         $quantity = intval($item['quantity'] ?? 1);
         $total += ($price * $quantity * 100); // Convert to cents
      }
      // Add tax
      $tax_rate = floatval(get_option(BASE . 'helcim_tax_rate', 0.05));
      $tax = intval($total * $tax_rate);
      return $total + $tax;
   }
   /**
    * Create Helcim invoice
    */
   private function createHelcimInvoice(array $cart_items, array $customer_info, int $total): array|WP_Error
   {
      $line_items = [];
      foreach ($cart_items as $item) {
         $post_id = intval($item['id'] ?? 0);
         if (!$post_id) continue;
         $post = get_post($post_id);
         $meta = Meta::forPost($post_id);
         $line_items[] = [
            'description' => $post->post_title,
            'quantity' => intval($item['quantity'] ?? 1),
            'price' => floatval($meta->get('price')) * 100,
            'productCode' => get_post_meta($post_id, BASE . '_helcim_product_code', true) ?: 'WP-' . $post_id
         ];
      }
      // Get or create customer
      $customer_id = $this->getOrCreateHelcimCustomer($customer_info);
      return $this->postRequest('commerce/invoice', [
         'customerId' => $customer_id,
         'invoiceNumber' => 'INV-' . time(),
         'tipAmount' => 0,
         'depositAmount' => 0,
         'notes' => $customer_info['notes'] ?? '',
         'lineItems' => $line_items
      ]);
   }
   /**
    * Process Helcim payment
    */
   private function processHelcimPayment(string $payment_token, string $invoice_id, int $amount): array|WP_Error
   {
      return $this->postRequest('payment/purchase', [
         'paymentToken' => $payment_token,
         'amount' => $amount,
         'currency' => 'CAD',
         'invoiceId' => $invoice_id
      ]);
   }
   /**
    * Get or create Helcim customer
    */
   private function getOrCreateHelcimCustomer(array $customer_info): ?string
   {
      if (empty($customer_info['email'])) {
      $user = get_userdata($userId);
      if (!$user || empty($user->user_email)) {
         return null;
      }
      // Search for existing customer
      $existing = $this->searchHelcimCustomer($customer_info['email']);
      if ($existing) {
         return $existing['customerId'];
      $id = $this->getCustomerIdByEmail($user->user_email);
      if ($id) {
         $this->linkUserToCustomer($userId, $id);
      }
      // Create new customer
      $response = $this->postRequest('customer', [
         'customerCode' => 'GUEST-' . time(),
         'contactName' => $customer_info['name'] ?? '',
         'email' => $customer_info['email'],
         'phone' => $customer_info['phone'] ?? ''
      ]);
      if (!is_wp_error($response) && isset($response['customerId'])) {
         return $response['customerId'];
      }
      return null;
      return $id;
   }
   /*****************************************************************
    * VALIDATION
    *****************************************************************/
   /**
    * Save order to user meta
    * Validate a HelcimPay.js transaction using the secret token.
    *
    * After the frontend receives a SUCCESS message event, call this
    * server-side to verify the transaction hash.
    */
   private function saveOrderToUser(int $user_id, string $order_id): void
   public function validateTransaction(string $secretToken, array $transactionData): bool
   {
      $orders = get_user_meta($user_id, BASE . '_helcim_orders', true) ?: [];
      $orders[] = [
         'order_id' => $order_id,
         'date' => current_time('mysql')
      ];
      // Keep only last 50 orders
      if (count($orders) > 50) {
         $orders = array_slice($orders, -50);
      }
      update_user_meta($user_id, BASE . '_helcim_orders', $orders);
      $hash = hash('sha256', $secretToken . json_encode($transactionData));
      return hash_equals($hash, $transactionData['hash'] ?? '');
   }
   /*******************************************************************
    * WEBHOOKS
   *******************************************************************/
   /**
    * Handle order status
    */
   public function handleOrderStatus(WP_REST_Request $request): WP_REST_Response
   {
      $order_id = $request->get_param('order_id');
      if (!$order_id) {
         return new WP_REST_Response(['error' => 'Order ID required'], 400);
      }
      // Check cache first
      $cached_status = get_transient(BASE . 'helcim_order_' . $order_id);
      if ($cached_status !== false) {
         return new WP_REST_Response($cached_status);
      }
      // Fetch from Helcim
      $response = $this->getRequest('commerce/invoice/' . $order_id);
      if (is_wp_error($response)) {
         return new WP_REST_Response(['error' => 'Could not fetch order status'], 500);
      }
      $status = [
         'status' => $response['status'] ?? 'unknown',
         'eta' => $response['estimatedTime'] ?? null,
         'items' => $response['lineItems'] ?? []
      ];
      // Cache for 1 minute
      set_transient(BASE . 'helcim_order_' . $order_id, $status, MINUTE_IN_SECONDS);
      return new WP_REST_Response($status);
   }
   /******************************************************************
    * WEBHOOK HANDLING
    ******************************************************************/
   /**
    * Validate webhook signature
    * Validate Helcim webhook signature.
    *
    * Helcim signs webhooks with HMAC-SHA256 using:
    *   signedContent = "{webhook-id}.{webhook-timestamp}.{body}"
    *   key = base64_decode(verifierToken)
    *
    * Headers: webhook-id, webhook-timestamp, webhook-signature (v1,{base64hash})
    *
    * @see https://devdocs.helcim.com/docs/enabling-webhooks-for-transactions
    */
   protected function validateWebhook(array $payload): bool
   {
      $signature = $_SERVER['HTTP_HELCIM_SIGNATURE'] ?? '';
      $headers = $payload['_headers'] ?? [];
      if (!$signature || !$this->webhook_secret) {
      $webhookId        = $headers['webhook_id'][0]        ?? $headers['webhook-id']        ?? '';
      $webhookTimestamp  = $headers['webhook_timestamp'][0] ?? $headers['webhook-timestamp'] ?? '';
      $webhookSignature = $headers['webhook_signature'][0]  ?? $headers['webhook-signature'] ?? '';
      if (empty($webhookId) || empty($webhookTimestamp) || empty($webhookSignature)) {
         $this->logError('Webhook missing required headers', [], 'warning');
         return false;
      }
      $body = file_get_contents('php://input');
      $expected = hash_hmac('sha256', $body, $this->webhook_secret);
      // Verify timestamp is within 5 minutes (prevent replay attacks)
      $now = time();
      if (abs($now - (int)$webhookTimestamp) > 300) {
         $this->logError('Webhook timestamp too old', [
            'webhook_timestamp' => $webhookTimestamp,
            'server_time'       => $now,
         ], 'warning');
         return false;
      }
      return hash_equals($expected, $signature);
      $secret = $this->getAdvancedSetting('webhook_signature_key');
      if (empty($secret)) {
         // If no signature key configured, allow webhook but log warning
         $this->logDebug('No webhook signature key configured — skipping verification');
         return true;
      }
      // Reconstruct the raw body from the payload (minus our injected _headers)
      $body = $payload['_raw_body'] ?? json_encode(
         array_diff_key($payload, ['_headers' => 1, '_raw_body' => 1])
      );
      $signedContent = "{$webhookId}.{$webhookTimestamp}.{$body}";
      $secretBytes   = base64_decode($secret);
      $expectedHash  = base64_encode(
         hash_hmac('sha256', $signedContent, $secretBytes, true)
      );
      // webhook-signature may contain multiple signatures: "v1,hash1 v2,hash2"
      $signatures = explode(' ', $webhookSignature);
      foreach ($signatures as $sig) {
         $parts = explode(',', $sig, 2);
         if (count($parts) === 2 && hash_equals($expectedHash, $parts[1])) {
            return true;
         }
      }
      $this->logError('Webhook signature mismatch', [
         'webhook_id' => $webhookId,
      ], 'warning');
      return false;
   }
   /**
    * Process webhook event
    * Process a validated Helcim webhook event.
    *
    * Helcim sends minimal payloads: {"id": "12345", "type": "cardTransaction"}
    * We fetch the full transaction details from the API.
    */
   protected function processWebhook(array $payload): bool
   {
      $event_type = $payload['eventType'] ?? '';
      $data = $payload['data'] ?? [];
      $type = $payload['type'] ?? '';
      $id   = $payload['id']  ?? '';
      switch ($event_type) {
         case 'transaction.success':
         case 'transaction.declined':
            return $this->handleTransactionWebhook($data);
         case 'invoice.paid':
         case 'invoice.updated':
            return $this->handleInvoiceWebhook($data);
         case 'customer.created':
         case 'customer.updated':
            return $this->handleCustomerWebhook($data);
         default:
            $this->logDebug('Unhandled webhook type', ['type' => $event_type]);
            return true;
      }
   }
   /**
    * Handle transaction webhook
    */
   private function handleTransactionWebhook(array $data): bool
   {
      $transaction_id = $data['transactionId'] ?? '';
      $status = $data['status'] ?? '';
      if (!$transaction_id) {
      if (empty($type) || empty($id)) {
         $this->logError('Webhook missing type or id', $payload, 'warning');
         return false;
      }
      // Update cached transaction status
      set_transient(BASE . 'helcim_transaction_' . $transaction_id, $status, HOUR_IN_SECONDS);
      // Trigger action for other integrations
      do_action(BASE . 'helcim_transaction_updated', $transaction_id, $status, $data);
      return true;
      return match ($type) {
         'cardTransaction' => $this->handleTransactionWebhook($id),
         'terminalCancel'  => $this->handleTerminalCancelWebhook($payload),
         default           => $this->handleUnknownWebhook($type, $id),
      };
   }
   /**
    * Handle invoice webhook
    * Handle a cardTransaction webhook — fetch full transaction, update records.
    */
   private function handleInvoiceWebhook(array $data): bool
   protected function handleTransactionWebhook(string $transactionId): bool
   {
      $invoice_id = $data['invoiceId'] ?? '';
      $status = $data['status'] ?? '';
      // Fetch full transaction from Helcim API
      $transaction = $this->getRequest("card-transactions/{$transactionId}");
      if (!$invoice_id) {
      if (is_wp_error($transaction) || empty($transaction)) {
         $this->logError('Failed to fetch transaction for webhook', [
            'transaction_id' => $transactionId,
         ]);
         return false;
      }
      // Update cached order status
      set_transient(BASE . 'helcim_order_' . $invoice_id, $status, HOUR_IN_SECONDS);
      $status = $transaction['status'] ?? '';
      // Trigger action for other integrations
      do_action(BASE . 'helcim_order_updated', $invoice_id, $status, $data);
      // Fire action for other parts of the system to react
      do_action('jvb_helcim_transaction', $transaction, $status);
      return true;
   }
   /**
    * Handle customer webhook
    */
   private function handleCustomerWebhook(array $data): bool
   {
      $customer_id = $data['customerId'] ?? '';
      $email = $data['email'] ?? '';
      if (!$customer_id || !$email) {
         return false;
      // If linked to an invoice, update invoice cache
      $invoiceNumber = $transaction['invoiceNumber'] ?? '';
      if (!empty($invoiceNumber)) {
         $this->cache->delete("invoice_{$invoiceNumber}");
         do_action('jvb_helcim_invoice_updated', $invoiceNumber, $transaction);
      }
      // Find WordPress user with this Helcim customer ID
      $users = get_users([
         'meta_key' => BASE . '_helcim_customer_id',
         'meta_value' => $customer_id,
         'number' => 1
      // Log for debugging
      $this->logDebug('Transaction webhook processed', [
         'transaction_id' => $transactionId,
         'status'         => $status,
         'amount'         => $transaction['amount'] ?? 0,
         'type'           => $transaction['type'] ?? '',
      ]);
      if (!empty($users)) {
         $user = $users[0];
         update_user_meta($user->ID, BASE . '_helcim_customer_updated', current_time('mysql'));
      return true;
   }
         // Clear cached customer data
         $this->cache->forget('helcim_customer_' . $user->ID);
      }
   /**
    * Handle Smart Terminal cancel webhook
    */
   protected function handleTerminalCancelWebhook(array $payload): bool
   {
      do_action('jvb_helcim_terminal_cancel', $payload);
      $this->logDebug('Terminal cancel webhook processed', [
         'payload' => $payload,
      ]);
      return true;
   }
   /******************************************************************
    * CONNECTION TESTING
    ******************************************************************/
   /**
    * Handle unknown webhook types (future-proofing)
    */
   protected function handleUnknownWebhook(string $type, string $id): bool
   {
      $this->logDebug('Unknown webhook type received', [
         'type' => $type,
         'id'   => $id,
      ]);
      do_action("jvb_helcim_webhook_{$type}", $id);
      return true;
   }
   /**
    * Perform connection test
    * Extract unique webhook ID for deduplication
    */
   protected function performConnectionTest(): bool
   protected function extractWebhookId(array $payload): ?string
   {
      if (empty($this->api_token) || empty($this->account_id)) {
         throw new Exception('Missing required credentials');
      $headers = $payload['_headers'] ?? [];
      return $headers['webhook_id'][0] ?? $headers['webhook-id'] ?? $payload['id'] ?? null;
   }
   /**
    * Override the webhook request handler to capture raw body for signature verification
    */
   public function handleWebhookRequest(\WP_REST_Request $request): \WP_REST_Response
   {
      $payload = $request->get_params();
      $payload['_headers']  = $request->get_headers();
      $payload['_raw_body'] = $request->get_body();
      $success = $this->handleWebhook($payload);
      return new \WP_REST_Response([
         'success' => $success,
      ], $success ? 200 : 400);
   }
   /***********************************************************************
    * POST HOOKS
   ***********************************************************************/
   /**
    * Sync a WordPress post to Helcim as a product.
    * Called by IntegrationExecutor::processSyncTo()
    */
   public function syncPostToService(int $postID): array|\WP_Error
   {
      $post = get_post($postID);
      if (!$post) {
         return new \WP_Error('not_found', "Post {$postID} not found");
      }
      $response = $this->getRequest('account');
      $helcimProductId = get_post_meta($postID, BASE . '_helcim_item_id', true);
      $productData     = $this->buildProductPayload($postID);
      if (is_wp_error($productData)) {
         return $productData;
      }
      if ($helcimProductId) {
         // Update existing
         $response = $this->patchRequest("products/{$helcimProductId}", $productData);
      } else {
         // Create new
         $response = $this->postRequest('products', $productData);
      }
      if (is_wp_error($response)) {
         throw new Exception($response->get_error_message());
         update_post_meta($postID, BASE . '_helcim_sync_status', 'error');
         return $response;
      }
      return isset($response['accountId']);
      // Store Helcim product ID
      $newId = $response['id'] ?? $response['productId'] ?? $helcimProductId;
      update_post_meta($postID, BASE . '_helcim_item_id', $newId);
      update_post_meta($postID, BASE . '_helcim_sync_status', 'synced');
      update_post_meta($postID, BASE . '_helcim_last_sync', current_time('mysql'));
      return ['success' => true, 'helcim_id' => $newId];
   }
   /**
    * Get request headers
    * Delete a product from Helcim.
    * Called by IntegrationExecutor::processDeleteFrom()
    */
   protected function getRequestHeaders(): array
   public function deleteFromService(string $externalId): array|\WP_Error
   {
      $headers = [
         'Content-Type' => 'application/json',
         'Accept' => 'application/json'
      ];
      $response = $this->deleteRequest("products/{$externalId}");
      // Add authorization header
      if (!empty($this->api_token)) {
         $headers['api-token'] = $this->api_token;
      if (is_wp_error($response)) {
         return $response;
      }
      return $headers;
      return ['success' => true, 'deleted' => $externalId];
   }
   /******************************************************************
    * INTEGRATION ACTIONS
    ******************************************************************/
   protected function handleImportFromHelcim()
   {
      $this->queueOperation('import_catalog', [
         'user_id' => $this->userID
      ], [
         'priority' => 'normal'
      ]);
      return [
         'success'   => true,
         'message'   => 'Import synced'
      ];
   }
   protected function handleSyncToHelcim()
   {
      $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_helcim', [
         'items' => $post_ids,
         'user_id' => $this->userID
      ], [
         'priority' => 'normal'
      ]);
      return [
         'success' => true,
         'message' => sprintf('Queued %d items for sync to Helcim', count($post_ids))
      ];
   }
   /******************************************************************
    * ADMIN UI
    ******************************************************************/
   /**
    * Process sync customer operation
    * Build Helcim product payload from a WordPress post.
    */
   private function processSyncCustomer(array $data): array
   protected function buildProductPayload(int $postID): array|\WP_Error
   {
      $user_id = $data['user_id'] ?? 0;
      $meta = \JVBase\meta\Meta::forPost($postID);
      $post = get_post($postID);
      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 Helcim customer
      $helcim_customer_id = $this->getOrCreateHelcimCustomer([
         'email' => $user->user_email,
         'name' => $user->display_name
      ]);
      if ($helcim_customer_id) {
         update_user_meta($user_id, BASE . '_helcim_customer_id', $helcim_customer_id);
         return [
            'success' => true,
            'result' => ['customer_id' => $helcim_customer_id]
         ];
      $price = $meta->get('price');
      if (empty($price) || !is_numeric($price)) {
         return new \WP_Error('invalid_price', "Post {$postID} has no valid price");
      }
      return [
         'success' => false,
         'result' => ['error' => 'Could not sync customer']
         'name'        => $post->post_title,
         'description' => wp_strip_all_tags($post->post_content),
         'sku'         => $meta->get('sku') ?: "wp-{$postID}",
         'price'       => (float) $price,
         'taxExempt'   => (bool) $meta->get('tax_exempt'),
      ];
   }
}