From d7dbe7fee362d587dfc334135d9581b6216a4295 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 23 Nov 2025 04:13:56 +0000
Subject: [PATCH] =Timeline block, and feed block updated. DataStore.js refactored to not block rendering
---
inc/integrations/Square.php | 450 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 431 insertions(+), 19 deletions(-)
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 4eb7b1c..62bbeb4 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1,8 +1,10 @@
<?php
namespace JVBase\integrations;
+use JVBase\meta\MetaForm;
use JVBase\meta\MetaManager;
use Exception;
+use JVBase\registry\PostTypeRegistrar;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -71,6 +73,8 @@
$this->title = 'Square';
$this->icon = 'square-logo';
+ $this->refresh_interval = 7 * DAY_IN_SECONDS;
+
// Define credential fields
$this->fields = [
'environment' => [
@@ -171,6 +175,8 @@
'sync_to_square' => 'Sync Site to Square',
]
);
+
+ add_action('init', [$this, 'registerSquarePostTypes']);
}
/**
@@ -207,6 +213,134 @@
}
+ public function getSquarePostConfig(string $post = 'all'):array
+ {
+ $posts = [
+ '_sq_orders' => [
+ 'singular' => 'Square Order',
+ 'plural' => 'Square Orders',
+ 'public' => false,
+ 'fields' => [
+ 'post_title' => [
+ 'type' => 'text',
+ 'label' => 'Order Number'
+ ],
+ 'square_order_id' => [
+ 'type' => 'text',
+ 'label' => 'Square Order ID',
+ 'readonly' => true
+ ],
+ 'square_payment_id' => [
+ 'type' => 'text',
+ 'label' => 'Square Payment ID',
+ 'readonly' => true
+ ],
+ 'square_customer_id' => [
+ 'type' => 'text',
+ 'label' => 'Square Customer ID',
+ 'readonly' => true
+ ],
+ 'amount' => [
+ 'type' => 'number',
+ 'label' => 'Total Amount (cents)',
+ 'readonly' => true
+ ],
+ 'status' => [
+ 'type' => 'select',
+ 'label' => 'Order Status',
+ 'options' => [
+ 'PROPOSED' => 'Proposed',
+ 'RESERVED' => 'Reserved',
+ 'PREPARED' => 'Prepared (Ready for Pickup)',
+ 'COMPLETED' => 'Completed',
+ 'CANCELED' => 'Canceled'
+ ],
+ 'readonly' => true
+ ],
+ 'fulfillment_status' => [
+ 'type' => 'select',
+ 'label' => 'Fulfillment Status',
+ 'options' => [
+ 'PROPOSED' => 'Proposed',
+ 'RESERVED' => 'Reserved',
+ 'PREPARED' => 'Prepared',
+ 'COMPLETED' => 'Completed',
+ 'CANCELED' => 'Canceled',
+ 'FAILED' => 'Failed'
+ ],
+ 'readonly' => true
+ ],
+ 'pickup_time' => [
+ 'type' => 'datetime',
+ 'label' => 'Pickup Time'
+ ],
+ 'customer_email' => [
+ 'type' => 'email',
+ 'label' => 'Customer Email',
+ 'readonly' => true
+ ],
+ 'customer_name' => [
+ 'type' => 'text',
+ 'label' => 'Customer Name',
+ 'readonly' => true
+ ],
+ 'customer_phone' => [
+ 'type' => 'tel',
+ 'label' => 'Customer Phone',
+ 'readonly' => true
+ ],
+ 'special_instructions' => [
+ 'type' => 'textarea',
+ 'label' => 'Special Instructions',
+ 'readonly' => true
+ ],
+ 'items' => [
+ 'type' => 'repeater',
+ 'label' => 'Order Items',
+ 'readonly' => true,
+ 'fields' => [
+ 'name' => ['type' => 'text', 'label' => 'Item Name'],
+ 'quantity' => ['type' => 'number', 'label' => 'Quantity'],
+ 'price' => ['type' => 'number', 'label' => 'Price'],
+ 'note' => ['type' => 'text', 'label' => 'Note']
+ ]
+ ],
+ 'receipt_url' => [
+ 'type' => 'url',
+ 'label' => 'Receipt URL',
+ 'readonly' => true
+ ],
+ 'created_at' => [
+ 'type' => 'datetime',
+ 'label' => 'Created At',
+ 'readonly' => true
+ ],
+ 'updated_at' => [
+ 'type' => 'datetime',
+ 'label' => 'Last Updated',
+ 'readonly' => true
+ ]
+ ]
+ ]
+ ];
+
+ if ($post === 'all'){
+ return $posts;
+ }elseif(array_key_exists($post, $posts)) {
+ return $posts[$post];
+ }
+ return [];
+ }
+
+ public function registerSquarePostTypes():void
+ {
+ $squarePostTypes = $this->getSquarePostConfig();
+ foreach ($squarePostTypes as $slug => $config) {
+ $registrar = new PostTypeRegistrar($slug, $config);
+ $registrar->register();
+ }
+ }
+
/**
* Get request headers for API calls
*/
@@ -717,10 +851,10 @@
// User login tracking for security
add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
- add_action('jvbAdditionalActions', [$this, 'outputCheckout']);
-
// Enqueue checkout scripts
add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
+
+ add_filter('jvbAdditionalActions', [$this, 'outputCheckout']);
}
@@ -728,7 +862,7 @@
if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
return $actions;
}
-
+ $meta = new MetaForm();
$form = '<aside id="cart" class="right">
<form id="checkout" data-form-id="checkout" data-save="checkout">';
@@ -745,12 +879,37 @@
'description' => 'Securely checkout with your name, email, and payments processed by Square.',
'content' => '<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 Details</h3>
- <input type="time" name="pickup_time" min="11:00" max="20:00" required>
+ '.$meta->return('cart_name', null, [
+ 'type' => 'text',
+ 'label' => 'Your Name',
+ 'required' => true,
+ 'autocomplete' => 'name'
+ ]).
+ $meta->return('cart_email', null, [
+ 'type' => 'email',
+ 'label' => 'Your Email',
+ 'required' => true,
+ 'autocomplete'=> 'email',
+ ]).
+ $meta->return('cart_phone', null, [
+ 'type' => 'tel',
+ 'label' => 'Your Phone',
+ 'required' => true,
+ 'autocomplete'=> 'phone'
+ ]).'
+ <h3>Pickup Details</h3>'.
+ $meta->return('pickup_time', null, [
+ 'type' => 'datetime',
+ 'label' => 'Pickup Type',
+ 'min' => '11:00',
+ 'max' => '20:00',
+ 'required' => true,
+ ]).
+ $meta->return('special_instructions', null, [
+ 'type' => 'textarea',
+ 'label' => 'Special Instructions',
+ 'quill' => true,
+ ]).'
<textarea name="special_instructions" placeholder="Special instructions or dietary notes"></textarea>
</div>
@@ -768,7 +927,7 @@
'content' => $this->renderOrderStatus()
]
];
- $form .= jvbRenderTabs($tabs);
+ $form .= jvbRenderTabs($tabs, true);
$form .= '<div class="cart-total row end"><p class="tax">Tax: <span></span></p><p class="total">GRAND TOTAL: <span></span></p></div>
</form>
@@ -790,11 +949,11 @@
<label for="quantity"></label>
<div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="17" data-id="">
- <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus').'</button>
+ <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus-square').'</button>
<input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input">
- <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('add').'</button>
+ <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('plus-square').'</button>
</div>
</td>
<td class="price">
@@ -817,8 +976,8 @@
$actions[] = [
- 'button' => '<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' => '<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false">
+ '.jvbIcon('shopping-cart').'<span class="abs"></span><span class="abs count"></span>
</button>',
'content' => $form
];
@@ -1836,16 +1995,49 @@
/**
* Handle order status webhook
*/
+ /**
+ * Handle order status webhook - NOW UPDATES POST TYPE
+ */
private function handleOrderWebhook(array $data): bool
{
$order_id = $data['object']['order']['id'] ?? '';
$state = $data['object']['order']['state'] ?? '';
+ $fulfillments = $data['object']['order']['fulfillments'] ?? [];
if (!$order_id) {
return false;
}
- // Update cached order status
+ // Find the WP post for this order
+ $wp_order_id = get_option(BASE . 'square_order_map_' . $order_id);
+
+ if ($wp_order_id) {
+ // Update the post meta
+ $meta = new MetaManager($wp_order_id, 'post');
+ $updates = [
+ 'status' => $state,
+ 'updated_at' => current_time('mysql')
+ ];
+
+ // Extract fulfillment status and pickup time
+ if (!empty($fulfillments[0])) {
+ $fulfillment = $fulfillments[0];
+ $updates['fulfillment_status'] = $fulfillment['state'] ?? $state;
+
+ if (!empty($fulfillment['pickup_details']['pickup_at'])) {
+ $updates['pickup_time'] = $fulfillment['pickup_details']['pickup_at'];
+ }
+ }
+
+ $meta->setAll($updates);
+
+ // Trigger notification to customer if order is ready
+ if ($state === 'PREPARED') {
+ do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
+ }
+ }
+
+ // Also update transient cache for quick status checks
set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
// Trigger action for other integrations
@@ -1940,7 +2132,6 @@
'jvb-a11y',
'jvb-cache',
'jvb-tabs',
- 'jvb-modal',
'jvb-popup'
],
'1.0.0',
@@ -1957,13 +2148,15 @@
'jvb-square-checkout',
'squareConfig',
[
- 'isOpen' => jvbIsOpen(),
+ 'isOpen' => jvbIsOpen(),
'application_id' => $this->credentials['client_id'] ?? '',
'location_id' => $this->locationId,
'environment' => $this->environment,
'api_url' => rest_url('jvb/v1/square/'),
'nonce' => wp_create_nonce('wp_rest'),
- 'currency' => get_option(BASE . 'currency', 'CAD')
+ 'currency' => get_option(BASE . 'currency', 'CAD'),
+ 'is_logged_in' => is_user_logged_in(),
+ 'user_email' => is_user_logged_in() ? wp_get_current_user()->user_email : '' // NEW
]
);
}
@@ -2950,7 +3143,8 @@
'name' => $image_title ?: 'Image',
'caption' => $alt_text ?: ''
]
- ]
+ ],
+ 'object_id' => $supported_image_id
];
$body = $this->buildMultipartBody($file_path, $request_json, $boundary);
@@ -3279,4 +3473,222 @@
$this->deleteCredentials();
}
}
+
+ private function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
+ {
+ // Build line items for Square
+ $line_items = [];
+ foreach ($items as $item) {
+ $line_item = [
+ 'quantity' => (string)$item['quantity'], // MUST be string!
+ ];
+
+ // Use catalog_object_id if available (recommended)
+ if (!empty($item['catalog_object_id'])) {
+ $line_item['catalog_object_id'] = $item['catalog_object_id'];
+ $line_item['catalog_version'] = $item['catalog_version'] ?? null;
+ } else {
+ // Ad-hoc line item (not recommended - no tax/inventory automation)
+ $line_item['name'] = $item['name'];
+ $line_item['base_price_money'] = [
+ 'amount' => (int)$item['price'],
+ 'currency' => $this->getCurrency()
+ ];
+ }
+
+ if (!empty($item['note'])) {
+ $line_item['note'] = $item['note'];
+ }
+
+ $line_items[] = $line_item;
+ }
+
+ $order_data = [
+ 'idempotency_key' => wp_generate_uuid4(), // Different from payment idempotency key
+ 'order' => [
+ 'location_id' => $this->locationId,
+ 'line_items' => $line_items,
+ 'state' => 'OPEN'
+ ]
+ ];
+
+ // Add customer if available
+ if ($customer_id) {
+ $order_data['order']['customer_id'] = $customer_id;
+ }
+
+ // Add metadata
+ if (!empty($data['note'])) {
+ $order_data['order']['metadata'] = [
+ 'special_instructions' => $data['note']
+ ];
+ }
+
+ if (!empty($data['pickup_time'])) {
+ $order_data['order']['metadata']['pickup_time'] = $data['pickup_time'];
+ }
+
+ return $this->postRequest('orders', $order_data);
+ }
+
+ private function createSquarePayment(
+ string $source_id,
+ string $idempotency_key,
+ int $amount_cents,
+ string $order_id,
+ ?string $customer_id
+ ): array|WP_Error
+ {
+ $payment_data = [
+ 'idempotency_key' => $idempotency_key,
+ 'source_id' => $source_id,
+ 'amount_money' => [
+ 'amount' => $amount_cents, // Already in cents!
+ 'currency' => $this->getCurrency()
+ ],
+ 'order_id' => $order_id,
+ 'location_id' => $this->locationId,
+ 'autocomplete' => true, // Capture immediately
+ ];
+
+ // Add customer if available
+ if ($customer_id) {
+ $payment_data['customer_id'] = $customer_id;
+ }
+
+ // Add reference ID for tracking
+ $payment_data['reference_id'] = 'WP_' . time();
+
+ return $this->postRequest('payments', $payment_data);
+ }
+
+ private function saveOrderToWordPress(array $order_data): int
+ {
+ // Extract customer info
+ $customer_email = $order_data['customer']['email'] ?? '';
+ $customer_name = $order_data['customer']['name'] ?? '';
+
+ // Find or create WP user for logged-in association
+ $user_id = 0;
+ if ($customer_email) {
+ $user = get_user_by('email', $customer_email);
+ if ($user) {
+ $user_id = $user->ID;
+ // Store Square customer ID on user
+ if (!empty($order_data['square_customer_id'])) {
+ update_user_meta($user_id, BASE . '_square_customer_id', $order_data['square_customer_id']);
+ }
+ }
+ }
+
+ // Create order post
+ $order_post_id = wp_insert_post([
+ 'post_type' => BASE . '_sq_orders',
+ 'post_title' => 'Order #' . $order_data['square_order_id'],
+ 'post_status' => 'publish',
+ 'post_author' => $user_id // Associate with user if logged in
+ ]);
+
+ if (!$order_post_id || is_wp_error($order_post_id)) {
+ $this->logError('Failed to create order post', ['order_data' => $order_data]);
+ return 0;
+ }
+
+ // Save all order meta
+ $meta = new MetaManager($order_post_id, 'post');
+ $fields = $this->getSquarePostConfig('_sq_orders')['fields'];
+ unset($fields['post_title']);
+ $meta->setFieldConfig($fields);
+
+ $meta->setAll([
+ 'square_order_id' => $order_data['square_order_id'],
+ 'square_payment_id' => $order_data['square_payment_id'] ?? '',
+ 'square_customer_id' => $order_data['square_customer_id'] ?? '',
+ 'amount' => $order_data['amount'],
+ 'status' => $order_data['status'],
+ 'fulfillment_status' => $order_data['fulfillment_status'] ?? 'PROPOSED',
+ 'pickup_time' => $order_data['pickup_time'] ?? '',
+ 'customer_email' => $customer_email,
+ 'customer_name' => $customer_name,
+ 'customer_phone' => $order_data['customer']['phone'] ?? '',
+ 'special_instructions' => $order_data['note'] ?? '',
+ 'items' => $order_data['items'],
+ 'receipt_url' => $order_data['receipt_url'] ?? '',
+ 'created_at' => current_time('mysql'),
+ 'updated_at' => current_time('mysql')
+ ]);
+
+ // Index by Square order ID for quick webhook lookups
+ update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id);
+
+ return $order_post_id;
+ }
+
+ /**
+ * Get currency code
+ */
+ private function getCurrency(): string
+ {
+ return get_option(BASE . 'currency', 'CAD');
+ }
+
+ /**
+ * Get customer with saved cards (2025-compliant)
+ */
+ public function getUserCards(string $customer_id): array
+ {
+ $response = $this->getRequest('cards?customer_id=' . $customer_id);
+ return $response['cards'] ?? [];
+ }
+
+
+ public function getUserOrders(string $customer_email): array
+ {
+ // First get Square customer ID
+ $customer_response = $this->postRequest('customers/search', [
+ 'filter' => [
+ 'email_address' => ['exact' => $customer_email]
+ ]
+ ]);
+
+ $customer_id = $customer_response['customers'][0]['id'] ?? null;
+ if (!$customer_id) {
+ return [];
+ }
+
+ // Get their orders
+ $orders_response = $this->postRequest('orders/search', [
+ 'filter' => [
+ 'customer_filter' => [
+ 'customer_ids' => [$customer_id]
+ ]
+ ],
+ 'sort' => [
+ 'sort_field' => 'CREATED_AT',
+ 'sort_order' => 'DESC'
+ ],
+ 'limit' => 50
+ ]);
+
+ return $orders_response['orders'] ?? [];
+ }
+
+ public function checkOrderStatus(string $order_id): ?string
+ {
+ // Check transient cache first
+ $cached = get_transient(BASE . 'square_order_' . $order_id);
+ if ($cached) {
+ return $cached;
+ }
+
+ // Fetch from Square
+ $response = $this->getRequest('orders/' . $order_id);
+ if (!is_wp_error($response)) {
+ $state = $response['order']['state'] ?? null;
+ set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
+ return $state;
+ }
+
+ return null;
+ }
}
--
Gitblit v1.10.0