| | |
| | | <?php |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\managers\queue\executors\IntegrationExecutor; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\ui\Checkout; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | | use JVBase\integrations\Requests; |
| | | use JVBase\integrations\SyncToPost; |
| | | use JVBase\integrations\UserConnection; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | */ |
| | | 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'; |
| | | use Admin, Instance, OAuth, Requests, UserConnection, SyncToPost; |
| | | |
| | | 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 |
| | | protected function __construct() |
| | | { |
| | | $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->service_name = 'helcim'; |
| | | $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->icon = 'currency-dollar'; |
| | | |
| | | $this->canSync = [ |
| | | 'create' => true, |
| | | 'update' => true, |
| | | 'delete' => false, |
| | | 'initial' => true, |
| | | 'update' => true, |
| | | 'delete' => true, |
| | | ]; |
| | | |
| | | $this->hasWebhooks = false; |
| | | $this->baseUrl = 'https://api.helcim.com/v2'; |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineActions(); |
| | | |
| | | 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', |
| | | ]); |
| | | } |
| | | protected function defineFields():void |
| | | { |
| | | $api_token = $this->addField( |
| | | 'api_token', |
| | | 'API Token', |
| | | 'text', |
| | | [$this, 'testUserCanConnect'] |
| | | ); |
| | | $api_token->setSubType('password'); |
| | | $api_token->setRequired(); |
| | | $api_token->setHint('Found in Helcim Dashboard → Settings → API Access'); |
| | | |
| | | /***************************************************************** |
| | | * ABSTRACT IMPLEMENTATIONS |
| | | *****************************************************************/ |
| | | $currency = $this->addField( |
| | | 'currency', |
| | | 'Currency', |
| | | 'select', |
| | | [$this, 'testUserSetup'] |
| | | ); |
| | | $currency->setOptions([ |
| | | 'CAD' => 'CAD', |
| | | 'USD' => 'USD' |
| | | ]); |
| | | $currency->setDefault('CAD'); |
| | | |
| | | $feeSaver = $this->addField( |
| | | 'fee_saver', |
| | | 'Fee Saver', |
| | | 'true_false', |
| | | [$this, 'testUserSetup'] |
| | | ); |
| | | $feeSaver->setHint('Pass processing fees to customers? (Not compatible with Google Pay)'); |
| | | |
| | | $allowACH = $this->addField( |
| | | 'allow_ach', |
| | | 'Allow ACH/Bank Payments', |
| | | 'true_false', |
| | | [$this, 'testUserSetup'] |
| | | ); |
| | | $allowACH->setHint('Enable bank account payments alongside credit card'); |
| | | } |
| | | public function testUserCanConnect():bool |
| | | { |
| | | $user = get_current_user_id(); |
| | | return $this->userCanConnect($user); |
| | | } |
| | | public function testUserSetup():bool |
| | | { |
| | | return $this->isSetUp() && $this->testUserCanConnect() && !empty($this->getApiToken()); |
| | | } |
| | | |
| | | protected function defineInstructions():void |
| | | { |
| | | $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', |
| | | ]; |
| | | } |
| | | |
| | | protected function defineActions():void |
| | | { |
| | | $this->addAction( |
| | | 'Initialize Checkout', |
| | | [$this, 'initializeCheckout'], |
| | | 'initialize_checkout' |
| | | ); |
| | | } |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | if (empty($this->credentials)) { |
| | | $this->loadCredentials(); |
| | | } |
| | | // TODO: Implement initialize() method. |
| | | } |
| | | |
| | | $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 refreshAccessToken(OAuthCredentials $credentials): array |
| | | { |
| | | // TODO: Implement refreshAccessToken() method. |
| | | } |
| | | |
| | | protected function getRequestHeaders(): array |
| | | { |
| | | return [ |
| | | 'api-token' => $this->credentials['api_token'] ?? '', |
| | | 'Content-Type' => 'application/json', |
| | | 'Accept' => 'application/json', |
| | | ]; |
| | | // TODO: Implement getRequestHeaders() method. |
| | | } |
| | | |
| | | protected function performConnectionTest(): bool |
| | | protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array |
| | | { |
| | | 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; |
| | | } |
| | | // TODO: Implement handleResponse() method. |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * 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); |
| | | } |
| | | // TODO: Implement setContentTypes() method. |
| | | } |
| | | |
| | | public function getAdditionalFields(?string $content_type = null):array |
| | | protected function formatForService(int $itemID, string $type = 'post'): 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) |
| | | ); |
| | | // TODO: Implement formatForService() method. |
| | | } |
| | | |
| | | protected function getCustomerFields():array |
| | | /************************************************************************* |
| | | * UTILITY |
| | | *************************************************************************/ |
| | | protected function getApiToken():string|false |
| | | { |
| | | 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'), |
| | | ]; |
| | | return $this->loadCredentials()['api_token']??false; |
| | | } |
| | | } |