From a8ff85042ffbfe94b678fd818c02deb613bfd116 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 07 Aug 2026 20:42:54 +0000
Subject: [PATCH] =Continuing on with Integrations setup. Working on PostMark now
---
inc/integrations/services/Helcim.php | 1332 ---------------------------------------------------------
1 files changed, 4 insertions(+), 1,328 deletions(-)
diff --git a/inc/integrations/services/Helcim.php b/inc/integrations/services/Helcim.php
index 355e9c2..6c41266 100644
--- a/inc/integrations/services/Helcim.php
+++ b/inc/integrations/services/Helcim.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\integrations\services;
+use JVBase\integrations\Admin;
+use JVBase\integrations\Instance;
use JVBase\integrations\Integrations;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\TypeConfig;
@@ -24,1336 +26,10 @@
*/
class Helcim extends Integrations
{
- protected static string $syncCustomer = 'helcim_sync_customer';
- protected string $service_name = 'helcim';
- protected string|array $apiBase = 'https://api.helcim.com/v2';
-
- protected bool $isOAuthService = false;
-
- /**
- * 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
- ];
-
- protected static array $instances = [];
- public static function getInstance(?int $userID = null):self
- {
- $key = is_null($userID) ? 'base' : $userID;
- if (!array_key_exists($key, self::$instances)) {
- self::$instances[$key] = new self($userID);
- }
- return self::$instances[$key];
- }
-
- protected function __construct(?int $userID = null)
- {
- $this->title = 'Helcim';
- $this->icon = 'credit-card';
-
- $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->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' => false,
- ];
-
- $this->hasWebhooks = false;
-
- parent::__construct();
-
- // 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',
- ]);
- }
-
- /*****************************************************************
- * ABSTRACT IMPLEMENTATIONS
- *****************************************************************/
+ use Admin, Instance;
protected function initialize(): void
{
- if (empty($this->credentials)) {
- $this->loadCredentials();
- }
-
- $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
- {
- $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);
- }
- }
-
- public function getAdditionalFields(?string $content_type = null):array
- {
- if ($content_type === 'customer') {
- $array = $this->getCustomerFields();
- return array_combine(
- array_map(fn($k) => '_square_' . $k, array_keys($array)),
- $array
- );
- }
- return array_combine(
- array_map(fn($k) => 'hc_' . $k, array_keys($this->getHelcimMeta($content_type))),
- $this->getHelcimMeta($content_type)
- );
- }
-
- protected function getCustomerFields():array
- {
- return [];
- }
-
- /**
- * Get Helcim product meta fields by type.
- *
- * Used by Registrar.php when helcim is configured
- */
- 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
- {
- if (!$this->isSetUp()) {
- return;
- }
-
- add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
-
- // Shared checkout UI (replaces provider-specific outputCheckout)
- add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
-
- // 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 webhook endpoint (handled by parent)
- $this->registerWebhookEndpoint();
- }
-
- public function enqueueScripts(): void
- {
- // HelcimPay.js SDK
- wp_enqueue_script(
- 'helcim-pay-sdk',
- 'https://secure.helcim.app/helcim-pay/services/start.js',
- [],
- null,
- true
- );
-
- // 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-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');
- }
-
-
- protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
- {
- $queue = JVB()->queue();
-
- $queue->registry()->register(self::$import, new TypeConfig(
- executor: $executor,
- maxRetries: 3
- ));
-
- $queue->registry()->register(self::$syncCustomer, new TypeConfig(
- executor: $executor,
- maxRetries: 2
- ));
- }
-
- /**
- * 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
- {
-
- error_log('==== [Helcim]::handleTheSavePost ====');
- $this->queueOperation(self::$syncTo, [
- 'items' => [$postID],
- 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author
- ], [
- 'priority' => 'high',
- 'delay' => 30,
- ]);
- Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
- }
- /**
- * Handle post deletion
- */
- public function handleDeletePost(int $postID): void
- {
- $item_id = Meta::forPost($postID)->get("_{$this->service_name}_item_id");
-
- if (empty($item_id)) {
- return;
- }
- $this->queueOperation(self::$deleteFrom, [
- 'external_ids' => [$item_id],
- 'post_id' => $postID,
- ], [
- 'priority' => 'high',
- ]);
-
- }
-
- protected function handleImportFromHelcim(): array
- {
- $this->queueOperation('import_products', [
- 'user_id' => $this->userID,
- ], ['priority' => 'normal']);
-
- 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;
- }
-
- /**
- * Resolve customer ID from user meta, falling back to email lookup + auto-link.
- */
- public function resolveCustomerId(int $userId): ?int
- {
- $id = $this->getUserCustomerId($userId);
- if ($id) {
- return $id;
- }
-
- $user = get_userdata($userId);
- if (!$user || empty($user->user_email)) {
- return null;
- }
-
- $id = $this->getCustomerIdByEmail($user->user_email);
- if ($id) {
- $this->linkUserToCustomer($userId, $id);
- }
-
- return $id;
- }
-
- /*****************************************************************
- * VALIDATION
- *****************************************************************/
-
- /**
- * 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.
- */
- public function validateTransaction(string $secretToken, array $transactionData): bool
- {
- $hash = hash('sha256', $secretToken . json_encode($transactionData));
- return hash_equals($hash, $transactionData['hash'] ?? '');
- }
- /*******************************************************************
- * WEBHOOKS
- *******************************************************************/
- /**
- * 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
- {
- $headers = $payload['_headers'] ?? [];
-
- $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;
- }
-
- // 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;
- }
-
- $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 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
- {
- $type = $payload['type'] ?? '';
- $id = $payload['id'] ?? '';
-
- if (empty($type) || empty($id)) {
- $this->logError('Webhook missing type or id', $payload, 'warning');
- return false;
- }
-
- return match ($type) {
- 'cardTransaction' => $this->handleTransactionWebhook($id),
- 'terminalCancel' => $this->handleTerminalCancelWebhook($payload),
- default => $this->handleUnknownWebhook($type, $id),
- };
- }
-
- /**
- * Handle a cardTransaction webhook — fetch full transaction, update records.
- */
- protected function handleTransactionWebhook(string $transactionId): bool
- {
- // Fetch full transaction from Helcim API
- $transaction = $this->getRequest("card-transactions/{$transactionId}");
-
- if (is_wp_error($transaction) || empty($transaction)) {
- $this->logError('Failed to fetch transaction for webhook', [
- 'transaction_id' => $transactionId,
- ]);
- return false;
- }
-
- $status = $transaction['status'] ?? '';
-
- // Fire action for other parts of the system to react
- do_action('jvb_helcim_transaction', $transaction, $status);
-
- // 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);
- }
-
- // Log for debugging
- $this->logDebug('Transaction webhook processed', [
- 'transaction_id' => $transactionId,
- 'status' => $status,
- 'amount' => $transaction['amount'] ?? 0,
- 'type' => $transaction['type'] ?? '',
- ]);
-
- return true;
- }
-
- /**
- * 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;
- }
-
- /**
- * 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;
- }
-
- /**
- * Extract unique webhook ID for deduplication
- */
- protected function extractWebhookId(array $payload): ?string
- {
- $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");
- }
-
- $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)) {
- update_post_meta($postID, BASE . '_helcim_sync_status', 'error');
- return $response;
- }
-
- // 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];
- }
-
- /**
- * Delete a product from Helcim.
- * Called by IntegrationExecutor::processDeleteFrom()
- */
- public function deleteFromService(string $externalId): array|\WP_Error
- {
- $response = $this->deleteRequest("products/{$externalId}");
-
- if (is_wp_error($response)) {
- return $response;
- }
-
- return ['success' => true, 'deleted' => $externalId];
- }
-
- /**
- * Build Helcim product payload from a WordPress post.
- */
- protected function buildProductPayload(int $postID): array|\WP_Error
- {
- $meta = \JVBase\meta\Meta::forPost($postID);
- $post = get_post($postID);
-
- $price = $meta->get('price');
- if (empty($price) || !is_numeric($price)) {
- return new \WP_Error('invalid_price', "Post {$postID} has no valid price");
- }
-
- return [
- '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'),
- ];
+ // TODO: Implement initialize() method.
}
}
--
Gitblit v1.10.0