<?php
|
namespace JVBase\integrations;
|
|
use JVBase\meta\MetaManager;
|
use Exception;
|
use WP_Error;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Helcim Integration for JVBase
|
* Handles bidirectional sync, customer management, and order processing
|
*/
|
class Helcim extends Integrations
|
{
|
// Helcim-specific configuration
|
private string $api_token;
|
private string $account_id;
|
private string $terminal_id;
|
private string $webhook_secret;
|
|
// Field mapping cache
|
private array $field_mappings = [];
|
private array $category_cache = [];
|
|
// 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
|
|
public function __construct(?int $userID = null)
|
{
|
$this->service_name = 'helcim';
|
$this->title = 'Helcim';
|
$this->icon = 'currency-circle-dollar';
|
|
// Helcim API endpoints
|
$this->apiBase = [
|
'production' => 'https://api.helcim.com/v2',
|
'sandbox' => 'https://api-sandbox.helcim.com/v2'
|
];
|
|
$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->canSync = [
|
'create' => true,
|
'update' => true,
|
'delete' => true
|
];
|
|
$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;
|
|
parent::__construct($userID);
|
|
$this->actions = array_merge(
|
$this->actions,
|
[
|
'import_from_helcim' => 'handleImportFromHelcim',
|
'sync_to_helcim' => 'handleSyncToHelcim'
|
]
|
);
|
// Initialize field mappings
|
$this->initializeFieldMappings();
|
}
|
|
/**
|
* Initialize service-specific settings
|
*/
|
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);
|
|
// 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,
|
];
|
}
|
|
/**
|
* Register additional WordPress hooks
|
*/
|
protected function registerAdditionalHooks(): void
|
{
|
// 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']);
|
}
|
|
/**
|
* 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);
|
}
|
}
|
}
|
|
/**
|
* 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('cart')?><span class="abs"></span><span class="abs count"></span>
|
</button>
|
<aside id="cart">
|
<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') ?>
|
</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
|
{
|
// 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';
|
|
wp_enqueue_script(
|
'helcim-js-sdk',
|
$sdk_url,
|
[],
|
null,
|
[
|
'strategy' => 'defer',
|
'in_footer' => true
|
]
|
);
|
|
// Register custom checkout script
|
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
|
]
|
);
|
|
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
|
******************************************************************/
|
|
/**
|
* Handle post save for Helcim 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],
|
'user_id' => $this->userID,
|
'content_type' => $settings['content_type'] ?? 'REGULAR'
|
], [
|
'priority' => 'high',
|
'delay' => 30, // Small delay to batch multiple saves
|
]);
|
|
update_post_meta($postID, BASE . '_helcim_sync_status', 'queued');
|
}
|
|
/**
|
* Handle post deletion
|
*/
|
public function handleDeletePost(int $postID): void
|
{
|
$helcim_id = get_post_meta($postID, BASE . '_helcim_product_id', true);
|
|
if ($helcim_id) {
|
$this->queueOperation('delete_from_helcim', [
|
'helcim_ids' => [$helcim_id],
|
'post_id' => $postID
|
], [
|
'priority' => 'high'
|
]);
|
}
|
}
|
|
/**
|
* Process queued operations
|
*/
|
public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
|
{
|
$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 = new MetaManager($post_id, 'post');
|
$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->getValue('price')) * 100, // Convert to cents
|
'taxable' => (bool)$meta->getValue('is_taxable'),
|
];
|
|
// Handle variations
|
$variations = $meta->getValue('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());
|
}
|
}
|
|
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 = new MetaManager($post_id, 'post');
|
$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) {
|
wp_mail(
|
$user->user_email,
|
'Security: Password Reset Required',
|
'For your security, please reset your password to continue accessing your account and saved payment methods.',
|
['Content-Type: text/html; charset=UTF-8']
|
);
|
}
|
}
|
|
/**
|
* 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(2, "wp-login.php?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 link below to set your password:\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" .
|
"Thanks,\n%s",
|
$site_name,
|
$reset_url,
|
$site_name
|
);
|
|
jvbMail(
|
$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 = new MetaManager($post_id, 'post');
|
$price = floatval($meta->getValue('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 = new MetaManager($post_id, 'post');
|
|
$line_items[] = [
|
'description' => $post->post_title,
|
'quantity' => intval($item['quantity'] ?? 1),
|
'price' => floatval($meta->getValue('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'])) {
|
return null;
|
}
|
|
// Search for existing customer
|
$existing = $this->searchHelcimCustomer($customer_info['email']);
|
|
if ($existing) {
|
return $existing['customerId'];
|
}
|
|
// 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;
|
}
|
|
/**
|
* Save order to user meta
|
*/
|
private function saveOrderToUser(int $user_id, string $order_id): void
|
{
|
$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);
|
}
|
|
/**
|
* 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
|
*/
|
protected function validateWebhook(array $payload): bool
|
{
|
$signature = $_SERVER['HTTP_HELCIM_SIGNATURE'] ?? '';
|
|
if (!$signature || !$this->webhook_secret) {
|
return false;
|
}
|
|
$body = file_get_contents('php://input');
|
$expected = hash_hmac('sha256', $body, $this->webhook_secret);
|
|
return hash_equals($expected, $signature);
|
}
|
|
/**
|
* Process webhook event
|
*/
|
protected function processWebhook(array $payload): bool
|
{
|
$event_type = $payload['eventType'] ?? '';
|
$data = $payload['data'] ?? [];
|
|
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) {
|
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;
|
}
|
|
/**
|
* Handle invoice webhook
|
*/
|
private function handleInvoiceWebhook(array $data): bool
|
{
|
$invoice_id = $data['invoiceId'] ?? '';
|
$status = $data['status'] ?? '';
|
|
if (!$invoice_id) {
|
return false;
|
}
|
|
// Update cached order status
|
set_transient(BASE . 'helcim_order_' . $invoice_id, $status, HOUR_IN_SECONDS);
|
|
// Trigger action for other integrations
|
do_action(BASE . 'helcim_order_updated', $invoice_id, $status, $data);
|
|
return true;
|
}
|
|
/**
|
* Handle customer webhook
|
*/
|
private function handleCustomerWebhook(array $data): bool
|
{
|
$customer_id = $data['customerId'] ?? '';
|
$email = $data['email'] ?? '';
|
|
if (!$customer_id || !$email) {
|
return false;
|
}
|
|
// Find WordPress user with this Helcim customer ID
|
$users = get_users([
|
'meta_key' => BASE . '_helcim_customer_id',
|
'meta_value' => $customer_id,
|
'number' => 1
|
]);
|
|
if (!empty($users)) {
|
$user = $users[0];
|
update_user_meta($user->ID, BASE . '_helcim_customer_updated', current_time('mysql'));
|
|
// Clear cached customer data
|
$this->cache->delete('helcim_customer_' . $user->ID);
|
}
|
|
return true;
|
}
|
|
/******************************************************************
|
* CONNECTION TESTING
|
******************************************************************/
|
|
/**
|
* Perform connection test
|
*/
|
protected function performConnectionTest(): bool
|
{
|
if (empty($this->api_token) || empty($this->account_id)) {
|
throw new Exception('Missing required credentials');
|
}
|
|
$response = $this->getRequest('account');
|
|
if (is_wp_error($response)) {
|
throw new Exception($response->get_error_message());
|
}
|
|
return isset($response['accountId']);
|
}
|
|
/**
|
* Get request headers
|
*/
|
protected function getRequestHeaders(): array
|
{
|
$headers = [
|
'Content-Type' => 'application/json',
|
'Accept' => 'application/json'
|
];
|
|
// Add authorization header
|
if (!empty($this->api_token)) {
|
$headers['api-token'] = $this->api_token;
|
}
|
|
return $headers;
|
}
|
|
/******************************************************************
|
* 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
|
*/
|
private function processSyncCustomer(array $data): array
|
{
|
$user_id = $data['user_id'] ?? 0;
|
|
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]
|
];
|
}
|
|
return [
|
'success' => false,
|
'result' => ['error' => 'Could not sync customer']
|
];
|
}
|
}
|