From df6c00db050e188a6bd5707e72c4f1f331ced923 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Feb 2026 20:46:43 +0000
Subject: [PATCH] =Port over to jakevan 2
---
inc/integrations/Helcim.php | 2634 ++++++++++++++++++++++++++---------------------------------
1 files changed, 1,173 insertions(+), 1,461 deletions(-)
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index a70ac16..d9e4e97 100644
--- a/inc/integrations/Helcim.php
+++ b/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'),
];
}
}
--
Gitblit v1.10.0