Changeset 3225869
- Timestamp:
- 01/21/2025 01:52:35 AM (14 months ago)
- Location:
- squarewoosync
- Files:
-
- 92 added
- 6 edited
-
tags/5.1.1 (added)
-
tags/5.1.1/assets (added)
-
tags/5.1.1/assets/banner-1544x500.jpg (added)
-
tags/5.1.1/assets/banner-772x250.jpg (added)
-
tags/5.1.1/assets/icon-128x128.png (added)
-
tags/5.1.1/assets/icon-256x256.png (added)
-
tags/5.1.1/assets/images (added)
-
tags/5.1.1/assets/images/box-outline.svg (added)
-
tags/5.1.1/assets/images/logo-new.svg (added)
-
tags/5.1.1/assets/images/logo.png (added)
-
tags/5.1.1/assets/js (added)
-
tags/5.1.1/assets/js/checkout-handler.js (added)
-
tags/5.1.1/assets/js/credit-card.js (added)
-
tags/5.1.1/assets/js/loyalty.js (added)
-
tags/5.1.1/assets/js/square-gateway.js (added)
-
tags/5.1.1/assets/js/sync-metabox.js (added)
-
tags/5.1.1/assets/js/utils.js (added)
-
tags/5.1.1/assets/js/wallets.js (added)
-
tags/5.1.1/assets/screenshot-1.png (added)
-
tags/5.1.1/assets/screenshot-2.png (added)
-
tags/5.1.1/assets/screenshot-3.png (added)
-
tags/5.1.1/assets/screenshot-4.png (added)
-
tags/5.1.1/assets/styles (added)
-
tags/5.1.1/assets/styles/checkout.css (added)
-
tags/5.1.1/assets/styles/loyalty.css (added)
-
tags/5.1.1/build (added)
-
tags/5.1.1/build/assets (added)
-
tags/5.1.1/build/assets/frontend (added)
-
tags/5.1.1/build/assets/frontend/wallet.asset.php (added)
-
tags/5.1.1/build/assets/frontend/wallet.css (added)
-
tags/5.1.1/build/assets/frontend/wallet.js (added)
-
tags/5.1.1/build/blocks (added)
-
tags/5.1.1/build/blocks/gateway.asset.php (added)
-
tags/5.1.1/build/blocks/gateway.js (added)
-
tags/5.1.1/build/blocks/loyalty.asset.php (added)
-
tags/5.1.1/build/blocks/loyalty.js (added)
-
tags/5.1.1/build/images (added)
-
tags/5.1.1/build/images/logo.4a5282be.png (added)
-
tags/5.1.1/build/index.asset.php (added)
-
tags/5.1.1/build/index.css (added)
-
tags/5.1.1/build/index.js (added)
-
tags/5.1.1/includes (added)
-
tags/5.1.1/includes/Abstracts (added)
-
tags/5.1.1/includes/Abstracts/RESTController.php (added)
-
tags/5.1.1/includes/Admin (added)
-
tags/5.1.1/includes/Admin/Menu.php (added)
-
tags/5.1.1/includes/Assets (added)
-
tags/5.1.1/includes/Assets/Manager.php (added)
-
tags/5.1.1/includes/Common (added)
-
tags/5.1.1/includes/Common/Keys.php (added)
-
tags/5.1.1/includes/Logger (added)
-
tags/5.1.1/includes/Logger/Logger.php (added)
-
tags/5.1.1/includes/Payments (added)
-
tags/5.1.1/includes/Payments/Blocks (added)
-
tags/5.1.1/includes/Payments/Blocks/WC_SquareSync_Gateway_Blocks_Support.php (added)
-
tags/5.1.1/includes/Payments/WC_SquareSync_Gateway.php (added)
-
tags/5.1.1/includes/REST (added)
-
tags/5.1.1/includes/REST/Api.php (added)
-
tags/5.1.1/includes/REST/LogController.php (added)
-
tags/5.1.1/includes/REST/OrdersController.php (added)
-
tags/5.1.1/includes/REST/SettingsController.php (added)
-
tags/5.1.1/includes/REST/SquareController.php (added)
-
tags/5.1.1/includes/Setup (added)
-
tags/5.1.1/includes/Setup/Installer.php (added)
-
tags/5.1.1/includes/Square (added)
-
tags/5.1.1/includes/Square/SquareHelper.php (added)
-
tags/5.1.1/includes/Square/SquareImport.php (added)
-
tags/5.1.1/includes/Square/SquareInventory.php (added)
-
tags/5.1.1/includes/Woo (added)
-
tags/5.1.1/includes/Woo/CreateProduct.php (added)
-
tags/5.1.1/includes/Woo/SyncProduct.php (added)
-
tags/5.1.1/includes/Woo/WooImport.php (added)
-
tags/5.1.1/languages (added)
-
tags/5.1.1/languages/square-woo-sync.pot (added)
-
tags/5.1.1/readme.txt (added)
-
tags/5.1.1/squarewoosync.php (added)
-
tags/5.1.1/templates (added)
-
tags/5.1.1/templates/app.php (added)
-
tags/5.1.1/vendor (added)
-
tags/5.1.1/vendor/autoload.php (added)
-
tags/5.1.1/vendor/bin (added)
-
tags/5.1.1/vendor/composer (added)
-
tags/5.1.1/vendor/composer/ClassLoader.php (added)
-
tags/5.1.1/vendor/composer/InstalledVersions.php (added)
-
tags/5.1.1/vendor/composer/LICENSE (added)
-
tags/5.1.1/vendor/composer/autoload_classmap.php (added)
-
tags/5.1.1/vendor/composer/autoload_namespaces.php (added)
-
tags/5.1.1/vendor/composer/autoload_psr4.php (added)
-
tags/5.1.1/vendor/composer/autoload_real.php (added)
-
tags/5.1.1/vendor/composer/autoload_static.php (added)
-
tags/5.1.1/vendor/composer/installed.json (added)
-
tags/5.1.1/vendor/composer/installed.php (added)
-
trunk/includes/REST/OrdersController.php (modified) (2 diffs)
-
trunk/includes/Woo/SyncProduct.php (modified) (2 diffs)
-
trunk/languages/square-woo-sync.pot (modified) (5 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/squarewoosync.php (modified) (3 diffs)
-
trunk/vendor/composer/installed.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
squarewoosync/trunk/includes/REST/OrdersController.php
r3198659 r3225869 100 100 } 101 101 102 103 $payment_gateways = WC()->payment_gateways->payment_gateways(); 104 102 105 $settings = get_option('square-woo-sync_settings', []); 103 if ( isset($settings['orders']['transactions']) && $settings['orders']['transactions'] === true) {106 if ((isset($settings['orders']['transactions']) && $settings['orders']['transactions'] === true) || (isset($payment_gateways['squaresync_credit']) && $payment_gateways['squaresync_credit']->enabled === 'yes')) { 104 107 $payResponse = $this->payForOrder($response['data']['order'], $square); 105 108 … … 280 283 $customer_note = $order->get_customer_note(); 281 284 if (!empty($customer_note)) { 282 $order_data['order']['note'] = mb_substr($customer_note, 0, 500); // Limit to 500 characters285 $order_data['order']['note'] = mb_substr($customer_note, 0, 500); // Limit to 500 characters 283 286 } 284 287 -
squarewoosync/trunk/includes/Woo/SyncProduct.php
r3145648 r3225869 6 6 use Pixeldev\SquareWooSync\REST\OrdersController; 7 7 use Pixeldev\SquareWooSync\Square\SquareHelper; 8 use Pixeldev\SquareWooSync\Woo\WooImport;9 8 10 9 if (!defined('ABSPATH')) { 11 exit; // Exit if accessed directly10 exit; // Exit if accessed directly 12 11 } 13 12 … … 17 16 class SyncProduct 18 17 { 19 private $customer_update_in_progress = false; 20 private $inventory_update_in_progress = false; 21 /** 22 * Constructor. 23 */ 24 public function __construct() 25 { 26 add_action('init', array($this, 'init_woo_product')); 18 private $inventory_update_in_progress = false; 19 /** 20 * Constructor. 21 */ 22 public function __construct() 23 { 24 add_action('init', array($this, 'init_woo_product')); 25 } 26 27 /** 28 * Initialize WooCommerce Product hooks. 29 */ 30 public function init_woo_product() 31 { 32 if (class_exists('WooCommerce')) { 33 34 add_action('sws_sync_order_after_product_sold_event', [$this, 'create_order_in_background']); 35 add_action('woocommerce_order_status_changed', array($this, 'create_square_order_after_woo_order'), 20, 4); 36 37 add_action('woocommerce_checkout_create_order', array($this, 'set_order_meta'), 10, 2); 38 } 39 } 40 41 public function set_order_meta($order, $data) 42 { 43 // Initialize an array to store all unique locations 44 $order_locations = []; 45 46 // Loop through each line item in the order 47 foreach ($order->get_items() as $item_id => $item) { 48 // Get the product ID 49 $product_id = $item->get_product_id(); 50 51 // Retrieve the 'square_locations' meta data for the product 52 $locations = get_post_meta($product_id, 'square_locations', true); 53 54 if (!empty($locations)) { 55 // Split the comma-separated locations into an array 56 $locations_array = explode(',', $locations); 57 58 // Merge with the existing order locations, ensuring uniqueness 59 $order_locations = array_unique(array_merge($order_locations, $locations_array)); 60 } 27 61 } 28 62 29 /** 30 * Initialize WooCommerce Product hooks. 31 */ 32 public function init_woo_product() 33 { 34 if (class_exists('WooCommerce')) { 35 add_action('add_meta_boxes', array($this, 'add_sync_meta_box')); 36 add_action('admin_post_sync_to_square', array($this, 'handle_sync_to_square')); 37 add_action('admin_post_export_to_square', array($this, 'handle_export_to_square')); 38 add_action('admin_footer', array($this, 'add_ajax_script')); 39 add_action('wp_ajax_sync_to_square', array($this, 'handle_ajax_sync_to_square')); 40 add_action('wp_ajax_export_to_square', array($this, 'handle_ajax_export_to_square')); 41 add_action('sws_sync_inventory_after_product_sold_event', [$this, 'sync_inventory_after_product_sold']); 42 add_action('sws_sync_order_after_product_sold_event', [$this, 'create_order_in_background']); 63 if (!empty($order_locations)) { 64 // Save the combined, unique locations as a comma-separated string 65 $order->update_meta_data('square_locations', implode(',', $order_locations)); 66 } 67 } 68 69 public function create_order_in_background($order_id) 70 { 71 $order = wc_get_order($order_id); 72 $uniqueProcessId = wp_generate_uuid4(); 43 73 44 74 45 add_action('woocommerce_order_status_changed', array($this, 'create_square_order_after_woo_order'), 20, 4); 75 Logger::log('info', 'Initiating order sync to Square for order #' . $order_id, array('process_id' => $uniqueProcessId)); 76 77 $ordersController = new OrdersController(); 78 $square = new SquareHelper(); 79 80 try { 81 // Retrieve or create Square customer ID 82 $square_customer_id = $ordersController->getOrCreateSquareCustomer($order, $square); 46 83 47 84 48 // Export product to Square on new Woo Product 49 add_action('transition_post_status', array($this, 'export_to_square'), 10, 31); 85 if (isset($square_customer_id['error'])) { 86 Logger::log('error', 'Square Orders error: ' . $square_customer_id['error'], array('parent_id' => $uniqueProcessId)); 87 } 50 88 51 // Update square customer 52 add_action('profile_update', array($this, 'update_square_customer'), 10, 1); 53 add_action('woocommerce_update_customer', array($this, 'update_square_customer'), 10, 1); 54 } 55 } 89 // Prepare order data for Square 90 $order_data = $ordersController->prepareSquareOrderData($order, $square_customer_id); 91 $response = $ordersController->createOrderInSquare($order_data, $square); 56 92 57 private function get_square_customer_payload($customer, $settings)58 {59 $first_name = sanitize_text_field($customer->get_first_name());60 $last_name = sanitize_text_field($customer->get_last_name());93 // Check for errors in the response 94 if (isset($response['error'])) { 95 Logger::log('error', 'Square Orders API error: ' . $response['error'], array('parent_id' => $uniqueProcessId)); 96 } 61 97 62 // Generate a unique idempotency key 63 $idempotency_key = uniqid('square_', true); 98 $payReponse = $ordersController->payForOrder($response['data']['order'], $square); 64 99 65 $payload = [66 'idempotency_key' => $idempotency_key,67 ];100 if (isset($payReponse['error'])) { 101 Logger::log('error', 'Square Payment API error: ' . $payReponse['error'], array('parent_id' => $uniqueProcessId)); 102 } 68 103 69 if (isset($settings['first_name']) && $settings['first_name'] === true && !empty($first_name)) { 70 $payload['given_name'] = $first_name; 104 105 if (isset($response['data']['order']['id']) && isset($payReponse['data']['payment']['id'])) { 106 $square_data = ['order' => $response, 'payment' => $payReponse]; 107 108 // Save Square order ID and payment ID to WooCommerce order meta 109 $order->update_meta_data('square_data', wp_json_encode($square_data)); 110 if (isset($response['data']['order']['id'])) { 111 $order->update_meta_data('square_order_id', $response['data']['order']['id']); 71 112 } 72 113 73 if (isset($settings['last_name']) && $settings['last_name'] === true && !empty($last_name)) {74 $payload['family_name'] = $last_name;75 }114 // Save changes to the order 115 $order->save(); 116 } 76 117 77 if (isset($settings['phone']) && $settings['phone'] === true && !empty($customer->get_billing_phone())) { 78 $payload['phone_number'] = $customer->get_billing_phone(); 79 } 118 // if (!empty($current_settings) || !empty($current_settings['loyalty']) || $current_settings['loyalty']['enabled'] === true) { 119 // $loyalty = new LoyaltyProgram(); 120 // $loyalty->accumulate_loylty_points($response['data']['order']['id'], $current_settings['loyalty']['program_id'], $square_customer_id); 121 // } 80 122 81 if (isset($settings['address']) && $settings['address'] === true) { 82 $address = []; 83 if (!empty($customer->get_billing_address_1())) { 84 $address['address_line_1'] = $customer->get_billing_address_1(); 85 } 86 if (!empty($customer->get_billing_address_2())) { 87 $address['address_line_2'] = $customer->get_billing_address_2(); 88 } 89 if (!empty($customer->get_billing_city())) { 90 $address['locality'] = $customer->get_billing_city(); 91 } 92 if (!empty($customer->get_billing_state())) { 93 $address['administrative_district_level_1'] = $customer->get_billing_state(); 94 } 95 if (!empty($customer->get_billing_postcode())) { 96 $address['postal_code'] = $customer->get_billing_postcode(); 97 } 98 if (!empty($customer->get_billing_country())) { 99 $address['country'] = $customer->get_billing_country(); 100 } 101 if (!empty($address)) { 102 $payload['address'] = $address; 103 } 104 } 105 106 return $payload; 123 Logger::log('success', 'Order and Transaction created in Square, receipt: #' . $payReponse['data']['payment']['receipt_number'], array('parent_id' => $uniqueProcessId)); 124 } catch (\Exception $e) { 125 if ($e->getMessage() == 'Square location not set') { 126 Logger::log('error', 'Square location not set: ' . $e->getMessage(), array('parent_id' => $uniqueProcessId)); 127 } 128 Logger::log('error', 'Failed to create order: ' . $e->getMessage(), array('parent_id' => $uniqueProcessId)); 107 129 } 108 109 function update_square_customer_roles($customer_id, $square_customer_id) 110 { 111 if ($this->customer_update_in_progress) { 112 return; 113 } 114 115 $this->customer_update_in_progress = true; 116 117 $user = get_user_by('id', $customer_id); 118 if ($user) { 119 $current_roles = $user->roles; 120 $group_ids_to_add = []; 121 $group_ids_to_remove = []; 122 123 // Get role mappings from settings 124 $settings = get_option('square-woo-sync_settings', []); 125 $role_mappings = $settings['customers']['roleMappings'] ?? []; 126 127 // Determine group IDs to add based on user's roles 128 foreach ($current_roles as $current_role) { 129 foreach ($role_mappings as $role => $mapping) { 130 if ($current_role === $role && isset($mapping['groupId'])) { 131 $group_ids_to_add[] = sanitize_text_field($mapping['groupId']); 132 } 133 } 134 } 135 136 // Ensure group IDs are unique and not empty 137 $group_ids_to_add = array_filter(array_unique($group_ids_to_add), function ($value) { 138 return !empty($value) && $value !== 'N/A'; 139 }); 140 141 // Determine group IDs to remove 142 foreach ($role_mappings as $role => $mapping) { 143 if (!in_array($role, $current_roles) && isset($mapping['groupId'])) { 144 $group_ids_to_remove[] = sanitize_text_field($mapping['groupId']); 145 } 146 } 147 148 $squareHelper = new SquareHelper(); 149 150 // Remove customer from groups 151 foreach ($group_ids_to_remove as $group_id) { 152 $remove_response = $squareHelper->square_api_request('/customers/' . $square_customer_id . '/groups/' . $group_id, 'DELETE'); 153 if (!$remove_response['success']) { 154 error_log('Failed to remove Square customer from group: ' . $group_id); 155 } 156 } 157 158 // Add customer to groups 159 foreach ($group_ids_to_add as $group_id) { 160 $group_response = $squareHelper->square_api_request('/customers/' . $square_customer_id . '/groups/' . $group_id, 'PUT'); 161 if (!$group_response['success']) { 162 error_log('Failed to add Square customer to group: ' . $group_id); 163 } 164 } 165 } 166 167 $this->customer_update_in_progress = false; 168 } 169 170 public function update_square_customer($customer_id) 171 { 172 $uniqueProcessId = wp_generate_uuid4(); 173 $logger = new Logger(); 174 175 // Get the customer data 176 $customer = new \WC_Customer($customer_id); 177 178 // Get settings to check if fields are active 179 $all_settings = get_option('square-woo-sync_settings', []); 180 $settings = $all_settings['customers']['auto']['wooSquare'] ?? []; 130 } 181 131 182 132 183 if (!$settings['is_active']) { 184 return; 185 } 133 /** 134 * Handle actions on order status change. 135 * 136 * @param int $order_id The order ID. 137 * @param string $old_status The old order status. 138 * @param string $new_status The new order status. 139 * @param $order The order object. 140 */ 141 public function create_square_order_after_woo_order($order_id, $old_status = '', $new_status = '', $order = null) 142 { 143 // Check if the event is already scheduled 144 $timestamp = wp_next_scheduled('sws_sync_inventory_after_product_sold_event', [$order_id]); 186 145 187 // Check if the update is coming from WooCommerce 188 if (get_user_meta($customer_id, '_update_source', true) === 'square') { 189 // Reset the source to avoid loops 190 update_user_meta($customer_id, '_update_source', ''); 191 return; 192 } 193 194 // Add a source identifier 195 update_user_meta($customer_id, '_update_source', 'woo'); 196 197 // Prepare customer data for update 198 $square_customer_id = get_user_meta($customer_id, 'square_customer_id', true); 199 if ($square_customer_id) { 200 $customer_data = $this->get_square_customer_payload($customer, $settings); 201 202 SquareHelper::queue_request('/customers/' . $square_customer_id, 'PUT', $customer_data, null, function ($response) use ($uniqueProcessId, $logger, $customer_id, $square_customer_id, $customer) { 203 if (!$response['success']) { 204 error_log('Failed to update Square customer: ' . json_encode($response['data'])); 205 206 $logger->log('error', 'Failed to update Square customer: ' . json_encode($response['data']), array('error_message' => json_encode($response['data']), 'process_id' => $uniqueProcessId)); 207 } else { 208 if (isset($settings['role']) && $settings['role'] === true) { 209 $this->update_square_customer_roles($customer_id, $square_customer_id); 210 } 211 $logger->log('success', 'Customer ' . $customer->get_email() . ' has been successfully updated in Square', array('process_id' => $uniqueProcessId)); 212 } 213 }); 214 } 215 } 216 217 /** 218 * Adds a meta box for syncing with Square. 219 */ 220 public function add_sync_meta_box() 221 { 222 add_meta_box( 223 'sws_sync_square', 224 'Sync with Square', 225 array($this, 'sync_meta_box_html'), 226 'product', 227 'side', 228 'high' 229 ); 230 } 231 232 /** 233 * Adds JavaScript for AJAX synchronization. 234 */ 235 public function add_ajax_script() 236 { 237 $screen = get_current_screen(); 238 if ('product' !== $screen->id) { 239 return; 240 } 241 242 $js_file_url = plugins_url('', SQUAREWOOSYNC_FILE) . '/assets/js/sync-metabox.js'; 243 244 wp_enqueue_script('sws-custom-script', $js_file_url, array('jquery'), '1.0', true); 245 wp_localize_script('sws-custom-script', 'swsAjax', array( 246 'ajaxurl' => admin_url('admin-ajax.php'), 247 'nonce' => wp_create_nonce('sws_ajax_nonce'), 248 )); 249 } 250 251 252 public function sync_inventory_after_product_sold($order_id) 253 { 254 $order = wc_get_order($order_id); 255 // Check if $order is a valid WC_Order object and return early if not 256 if ($this->inventory_update_in_progress) { 257 return; 258 } 259 260 // Get the current settings once, and return early if the sync isn't enabled or configured correctly 261 $current_settings = get_option('square-woo-sync_settings', []); 262 if (empty($current_settings) || !$current_settings['wooAuto']['isActive'] || !$current_settings['wooAuto']['stock']) { 263 return; 264 } 265 266 $this->inventory_update_in_progress = true; 267 268 // Initialize variables 269 $uniqueProcessId = wp_generate_uuid4(); 270 $hasSquareLinkedProduct = false; 271 $logger = null; // Defer logger initialization 272 $square_product_ids = []; // To store all square_product_ids with corresponding WooCommerce product IDs 273 274 // Loop through order items 275 foreach ($order->get_items() as $item) { 276 $product = $item->get_product(); 277 if (!$product || !$product->managing_stock()) { 278 continue; // Skip if the product is not managing stock 279 } 280 281 // Check if the product is a variation 282 if ($product->is_type('variation')) { 283 $parent_product = wc_get_product($product->get_parent_id()); 284 $square_product_id = $parent_product ? $parent_product->get_meta('square_product_id') : null; 285 } else { 286 $square_product_id = $product->get_meta('square_product_id'); 287 } 288 289 if (!$square_product_id) { 290 continue; // Skip if the product is not linked to Square 291 } 292 293 // Add the square_product_id and WooCommerce product ID to the list for syncing 294 $square_product_ids[] = [ 295 'square_product_id' => $square_product_id, 296 'woo_product_id' => $product->get_id() 297 ]; 298 299 // At least one product is linked to Square 300 $hasSquareLinkedProduct = true; 301 } 302 303 // Log the parent entry only if at least one product is linked to Square 304 if ($hasSquareLinkedProduct) { 305 if (!$logger) { 306 $logger = new Logger(); 307 } 308 $logger->log('info', 'Initiating inventory sync from WooCommerce to Square', ['process_id' => $uniqueProcessId]); 309 } 310 311 // Queue request to retrieve Square catalog objects 312 SquareHelper::queue_request('/catalog/batch-retrieve', 'POST', [ 313 'object_ids' => array_column($square_product_ids, 'square_product_id') 314 ], null, function ($response) use ($uniqueProcessId, $logger, $square_product_ids, $current_settings) { 315 if (!$logger) { 316 $logger = new Logger(); 317 } 318 319 if (!$response['success']) { 320 error_log('Failed to update Square inventory: ' . json_encode($response['data'])); 321 $logger->log('error', 'Failed to retrieve Square catalog: ' . json_encode($response['data']), [ 322 'error_message' => json_encode($response['data']), 323 'process_id' => $uniqueProcessId 324 ]); 325 $this->inventory_update_in_progress = false; 326 } else { 327 328 if (isset($response['data']['objects']) && !empty($response['data']['objects'])) { 329 330 $body = [ 331 'idempotency_key' => wp_generate_uuid4(), 332 'changes' => [] 333 ]; 334 335 foreach ($response['data']['objects'] as $square_object) { 336 // Ensure 'item_data' exists and has 'variations' 337 if (isset($square_object['item_data']['variations']) && is_array($square_object['item_data']['variations'])) { 338 foreach ($square_object['item_data']['variations'] as $variation) { 339 // Find the WooCommerce product ID associated with this Square variation 340 $woo_product_key = array_search($square_object['id'], array_column($square_product_ids, 'square_product_id')); 341 342 if ($woo_product_key !== false) { 343 $woo_product_id = $square_product_ids[$woo_product_key]['woo_product_id']; 344 $woo_product = wc_get_product($woo_product_id); 345 $stock_quantity = $woo_product->get_stock_quantity(); 346 347 // Add the change to the body array 348 if ($stock_quantity !== null) { 349 $body['changes'][] = [ 350 'physical_count' => [ 351 'quantity' => (string)$stock_quantity, 352 'occurred_at' => current_time('c'), 353 'location_id' => $current_settings['location'], 354 'catalog_object_id' => $variation['id'], 355 'state' => 'IN_STOCK' 356 ], 357 'type' => 'PHYSICAL_COUNT' 358 ]; 359 } else { 360 $logger->log('error', 'Stock quantity is null for product ID: ' . $woo_product_id, [ 361 'process_id' => $uniqueProcessId 362 ]); 363 } 364 } 365 } 366 } 367 } 368 369 // Ensure that there are changes to be synced 370 if (!empty($body['changes'])) { 371 // Queue the request to update the inventory on Square 372 SquareHelper::queue_request('/inventory/changes/batch-create', 'POST', $body, null, function ($response) use ($uniqueProcessId, $logger) { 373 if (!$logger) { 374 $logger = new Logger(); 375 } 376 377 if ($response['success']) { 378 $logger->log('success', 'Square inventory successfully updated.', [ 379 'process_id' => $uniqueProcessId 380 ]); 381 } else { 382 $logger->log('error', 'Failed to update Square inventory: ' . json_encode($response), [ 383 'error_message' => json_encode($response), 384 'process_id' => $uniqueProcessId 385 ]); 386 } 387 388 // Reset inventory update flag here 389 $this->inventory_update_in_progress = false; 390 }); 391 } else { 392 $logger->log('info', 'No valid inventory changes found to sync with Square.', [ 393 'process_id' => $uniqueProcessId 394 ]); 395 396 // Reset inventory update flag here 397 $this->inventory_update_in_progress = false; 398 } 399 } else { 400 $logger->log('info', 'No valid objects returned from Square API.', [ 401 'process_id' => $uniqueProcessId 402 ]); 403 404 // Reset inventory update flag here 405 $this->inventory_update_in_progress = false; 406 } 407 } 408 }); 409 } 410 411 412 public function create_order_in_background($order_id) 413 { 414 $order = wc_get_order($order_id); 415 $uniqueProcessId = wp_generate_uuid4(); 416 $logger = new Logger(); 417 418 $logger->log('info', 'Initiating order sync to Square for order #' . $order_id, array('process_id' => $uniqueProcessId)); 419 420 $ordersController = new OrdersController(); 421 $square = new SquareHelper(); 422 423 try { 424 // Retrieve or create Square customer ID 425 $square_customer_id = $ordersController->getOrCreateSquareCustomer($order, $square); 426 427 428 if (isset($square_customer_id['error'])) { 429 $logger->log('error', 'Square Orders error: ' . $square_customer_id['error'], array('parent_id' => $uniqueProcessId)); 430 } 431 432 // Prepare order data for Square 433 $order_data = $ordersController->prepareSquareOrderData($order, $square_customer_id); 434 $response = $ordersController->createOrderInSquare($order_data, $square); 435 436 // Check for errors in the response 437 if (isset($response['error'])) { 438 $logger->log('error', 'Square Orders API error: ' . $response['error'], array('parent_id' => $uniqueProcessId)); 439 } 440 441 $payReponse = $ordersController->payForOrder($response['data']['order'], $square); 442 443 if (isset($payReponse['error'])) { 444 $logger->log('error', 'Square Payment API error: ' . $payReponse['error'], array('parent_id' => $uniqueProcessId)); 445 } 446 447 448 if (isset($response['data']['order']['id']) && isset($payReponse['data']['payment']['id'])) { 449 $square_data = ['order' => $response, 'payment' => $payReponse]; 450 451 // Save Square order ID and payment ID to WooCommerce order meta 452 $order->update_meta_data('square_data', wp_json_encode($square_data)); 453 454 // Save changes to the order 455 $order->save(); 456 } 457 458 // if (!empty($current_settings) || !empty($current_settings['loyalty']) || $current_settings['loyalty']['enabled'] === true) { 459 // $loyalty = new LoyaltyProgram(); 460 // $loyalty->accumulate_loylty_points($response['data']['order']['id'], $current_settings['loyalty']['program_id'], $square_customer_id); 461 // } 462 463 $logger->log('success', 'Order and Transaction created in Square, receipt: #' . $payReponse['data']['payment']['receipt_number'], array('parent_id' => $uniqueProcessId)); 464 } catch (\Exception $e) { 465 if ($e->getMessage() == 'Square location not set') { 466 $logger->log('error', 'Square location not set: ' . $e->getMessage(), array('parent_id' => $uniqueProcessId)); 467 } 468 $logger->log('error', 'Failed to create order: ' . $e->getMessage(), array('parent_id' => $uniqueProcessId)); 469 } 470 } 471 472 473 /** 474 * Handle actions on order status change. 475 * 476 * @param int $order_id The order ID. 477 * @param string $old_status The old order status. 478 * @param string $new_status The new order status. 479 * @param $order The order object. 480 */ 481 public function create_square_order_after_woo_order($order_id, $old_status = '', $new_status = '', $order = null) 482 { 483 error_log('test'); 484 // Check if the event is already scheduled 485 $timestamp = wp_next_scheduled('sws_sync_inventory_after_product_sold_event', [$order_id]); 486 487 if (!$timestamp) { 488 // Schedule the sync_inventory_after_product_sold function to run in the background 489 wp_schedule_single_event(time() + 10, 'sws_sync_inventory_after_product_sold_event', [$order_id]); 490 } 491 492 493 494 // Check if the order already has Square data to prevent duplication 495 if ($order && $order->get_meta('square_data')) { 496 return; 497 } 498 499 $current_settings = get_option('square-woo-sync_settings', []); 500 501 if (empty($current_settings) || empty($current_settings['orders']) || $current_settings['orders']['enabled'] !== true) { 502 return; 503 } 504 505 if (empty($current_settings) || empty($current_settings['orders']) || $current_settings['orders']['stage'] !== $new_status) { 506 return; 507 } 508 509 // Check if the event is already scheduled 510 $timestamp = wp_next_scheduled('sws_sync_order_after_product_sold_event', [$order_id]); 511 512 if (!$timestamp) { 513 // Schedule the sync_inventory_after_product_sold function to run in the background 514 wp_schedule_single_event(time(), 'sws_sync_order_after_product_sold_event', [$order_id]); 515 } 516 } 517 518 /** 519 * AJAX handler for exporting products to Square. 520 */ 521 public function handle_ajax_export_to_square() 522 { 523 check_ajax_referer('sws_ajax_nonce', 'nonce'); 524 525 $product_id = intval($_POST['product_id']); 526 $product = wc_get_product($product_id); 527 528 $uniqueProcessId = wp_generate_uuid4(); 529 530 if ($product_id && $product) { 531 $exporter = new WooImport(); 532 533 $result = $exporter->import_products([$product], 1, $uniqueProcessId); 534 $exporter->update_square_inventory_counts([$product]); 535 536 537 if (is_array($result) && isset($result[0]) && is_array($result[0]) && isset($result[0]['success']) && $result[0]['success'] === true) { 538 wp_send_json_success(array('message' => 'Successfully exported and linked product to Square.')); 539 } else { 540 wp_send_json_error(array('message' => json_encode($result))); 541 } 542 } else { 543 wp_send_json_error(array('message' => 'Invalid product ID.')); 544 } 545 } 546 547 /** 548 * Delete Square Product from Woo ID 549 */ 550 public function delete_square_product($post_id) 551 { 552 // Check if the post type is 'product' 553 if (get_post_type($post_id) === 'product') { 554 555 // Get the current settings 556 $current_settings = get_option('square-woo-sync_settings', []); 557 558 // Check if auto product deletion is enabled in the settings 559 if (!empty($current_settings) && !empty($current_settings['wooAuto']['autoDeleteProduct']) && $current_settings['wooAuto']['autoDeleteProduct'] === true) { 560 561 // Get the square_product_id from post meta 562 $square_product_id = get_post_meta($post_id, 'square_product_id', true); 563 564 if ($square_product_id) { 565 $product = wc_get_product($post_id); 566 $product_name = $product ? $product->get_name() : 'Unknown Product'; 567 568 $logger = new Logger(); 569 $square_helper = new SquareHelper(); 570 $uniqueProcessId = wp_generate_uuid4(); 571 572 try { 573 // Log the initiation of the deletion process 574 $logger->log('info', 'Initiating deletion of Square product ' . $product_name, array( 575 'product_id' => $post_id, 576 'product_name' => $product_name, 577 'square_product_id' => $square_product_id, 578 'process_id' => $uniqueProcessId 579 )); 580 581 // Send a request to the Square API to delete the product 582 $response = $square_helper->square_api_request("/catalog/object/" . $square_product_id, 'DELETE'); 583 584 if (!$response['success']) { 585 // Log the error if the deletion failed 586 $logger->log('error', 'Failed to delete ' . $product_name . ' from Square library', array( 587 'product_id' => $post_id, 588 'product_name' => $product_name, 589 'square_product_id' => $square_product_id, 590 'error_message' => $response['error'], 591 'parent_id' => $uniqueProcessId 592 )); 593 } else { 594 // Log the success if the deletion was successful 595 $logger->log('success', 'Successfully deleted ' . $product_name . ' from Square library', array( 596 'product_id' => $post_id, 597 'product_name' => $product_name, 598 'square_product_id' => $square_product_id, 599 'parent_id' => $uniqueProcessId 600 )); 601 } 602 } catch (\Exception $e) { 603 // Log the exception if an error occurred during the deletion process 604 $logger->log('error', 'Exception occurred while deleting Square product', array( 605 'product_id' => $post_id, 606 'product_name' => $product_name, 607 'square_product_id' => $square_product_id, 608 'error_message' => $e->getMessage(), 609 'process_id' => $uniqueProcessId 610 )); 611 } 612 } 613 } 614 } 146 if (!$timestamp) { 147 // Schedule the sync_inventory_after_product_sold function to run in the background 148 wp_schedule_single_event(time() + 10, 'sws_sync_inventory_after_product_sold_event', [$order_id]); 615 149 } 616 150 617 151 618 152 153 // Check if the order already has Square data to prevent duplication 154 if ($order && $order->get_meta('square_data')) { 155 return; 156 } 157 158 $current_settings = get_option('square-woo-sync_settings', []); 159 $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); 619 160 620 161 621 622 623 /** 624 * AJAX handler for syncing products to Square. 625 */ 626 public function handle_ajax_sync_to_square() 627 { 628 check_ajax_referer('sws_ajax_nonce', 'nonce'); 629 630 $logger = new Logger(); 631 632 $product_id = intval($_POST['product_id']); 633 634 if ($product_id) { 635 636 $product = wc_get_product($product_id); 637 $data_to_import = array( 638 'stock' => true, 639 'title' => true, 640 'description' => true, 641 'price' => true, 642 'sku' => true, 643 ); 644 645 646 647 $result = $this->on_product_update($product_id, $data_to_import, true); 648 649 if ($result && $this->is_sync_successful($result)) { 650 $logger->log('success', 'Successfully synced: ' . $product->get_title() . ' to Square', array('product_id' => $product_id)); 651 wp_send_json_success(array('message' => 'Product synced successfully with Square.')); 652 } else { 653 wp_send_json_error(array('message' => $result['error'])); 654 } 655 } else { 656 wp_send_json_error(array('message' => 'Invalid product ID.')); 657 } 162 if (empty($current_settings) || empty($current_settings['orders']) || $current_settings['orders']['enabled'] !== true && !isset($available_gateways['squaresync_credit'])) { 163 return; 658 164 } 659 165 660 /** 661 * Check if sync result is successful. 662 * 663 * @param array $result The result array. 664 * 665 * @return bool 666 */ 667 private function is_sync_successful($result) 668 { 669 return (isset($result['inventoryUpdateStatus']['success']) && $result['inventoryUpdateStatus']['success'] === true) || 670 (isset($result['productUpdateStatus']['success']) && $result['productUpdateStatus']['success'] === true); 166 if (empty($current_settings) || empty($current_settings['orders']) || $current_settings['orders']['stage'] !== $new_status) { 167 return; 671 168 } 672 169 170 // Check if the event is already scheduled 171 $timestamp = wp_next_scheduled('sws_sync_order_after_product_sold_event', [$order_id]); 673 172 674 /** 675 * Renders the HTML for the meta box. 676 * 677 * @param WP_Post $post The post object. 678 */ 679 public function sync_meta_box_html($post) 680 { 681 $square_product_id = get_post_meta($post->ID, 'square_product_id', true); 682 683 if (!empty($square_product_id)) { 684 echo '<p>' . esc_html__('Sync this product to Square', 'squarewoosync') . '</p>'; 685 echo '<button id="sync_to_square_button" class="update-button button button-primary button-large" data-product-id="' . esc_attr($post->ID) . '">' . esc_html__('Sync to Square', 'squarewoosync') . '</button>'; 686 echo '<p class="sws-notice">' . esc_html__('Update the product and then run the above sync. For a full tutorial, please read the documentation.', 'squarewoosync') . '</p>'; 687 } else { 688 echo '<p>' . esc_html__('No Square product ID found. Unable to sync to square. Only products imported from square can be synced.', 'squarewoosync') . '</p>'; 689 echo '<button id="export_to_square_button" class="update-button button button-primary button-large" data-product-id="' . esc_attr($post->ID) . '">' . esc_html__('Export to Square', 'squarewoosync') . '</button>'; 690 } 173 if (!$timestamp) { 174 // Schedule the sync_inventory_after_product_sold function to run in the background 175 wp_schedule_single_event(time(), 'sws_sync_order_after_product_sold_event', [$order_id]); 691 176 } 692 693 694 /** 695 * Handles the product update process. 696 * 697 * @param int $product_id The product ID. 698 * @return mixed 699 */ 700 public function on_product_update($product_id, $data_to_import, $force = false) 701 { 702 703 704 $settings = get_option('square-woo-sync_settings', []); 705 706 if (empty($settings['wooAuto']) && !$force) { 707 return null; 708 } 709 710 $product = wc_get_product($product_id); 711 712 713 if (!$product instanceof \WC_Product) { 714 return null; // Optionally log this error. 715 } 716 717 $square_product_id = get_post_meta($product_id, 'square_product_id', true); 718 $woo_data = $this->get_woo_product_data($product, $square_product_id); 719 if ($square_product_id && !empty($woo_data)) { 720 return $this->update_square_product($square_product_id, $woo_data, $data_to_import); 721 } 722 } 723 724 /** 725 * Retrieves WooCommerce product data. 726 * 727 * @param \WC_Product $product WooCommerce product object. 728 * @param string $square_product_id Square product ID. 729 * @return array 730 */ 731 public function get_woo_product_data(\WC_Product $product, $square_product_id) 732 { 733 $woo_data = [ 734 'name' => $product->get_name(), 735 'description' => $product->get_description(), 736 'variations' => [] 737 ]; 738 739 if ($product->is_type('variable')) { 740 foreach ($product->get_children() as $variation_id) { 741 $variation = wc_get_product($variation_id); 742 if (!$variation) { 743 continue; 744 } 745 746 $variation_product_id = get_post_meta($variation->get_id(), 'square_product_id', true); 747 $woo_data['variations'][] = $this->format_variation_data($variation, $variation_product_id); 748 } 749 } else { 750 $woo_data['variations'][] = $this->format_variation_data($product, $square_product_id); 751 } 752 753 return $woo_data; 754 } 755 756 /** 757 * Formats variation data for synchronization. 758 * 759 * @param \WC_Product $product WooCommerce product object. 760 * @param string $square_product_id Square product ID. 761 * @return array 762 */ 763 private function format_variation_data(\WC_Product $product, $square_product_id) 764 { 765 return [ 766 'price' => $product->get_price(), 767 'sku' => $product->get_sku(), 768 'stock' => $product->get_stock_quantity(), 769 'square_id' => $square_product_id, 770 ]; 771 } 772 773 /** 774 * Updates the Square product with WooCommerce data. 775 * 776 * @param string $square_product_id Square product ID. 777 * @param array $woo_data WooCommerce product data. 778 * @return mixed 779 */ 780 public function update_square_product($square_product_id, $woo_data, $data_to_import) 781 { 782 $square_helper = new SquareHelper(); 783 $square_product_data = $square_helper->get_square_item_details($square_product_id); 784 785 if (isset($square_product_data['object']) && isset($square_product_data['object']['type'])) { 786 // Check if it's an ITEM or ITEM_VARIATION and proceed accordingly 787 if ($square_product_data['object']['type'] === 'ITEM') { 788 if (count($woo_data['variations']) === 1 && isset($square_product_data['object']['item_data']['variations'][0]['id'])) { 789 $woo_data['variations'][0]['square_id'] = $square_product_data['object']['item_data']['variations'][0]['id']; 790 } 791 } elseif ($square_product_data['object']['type'] === 'ITEM_VARIATION' && isset($square_product_data['object']['item_variation_data']['id'])) { 792 // Assuming the structure to access the ID for an ITEM_VARIATION is correct 793 // Adjust based on actual structure if needed 794 if (count($woo_data['variations']) === 1) { 795 $woo_data['variations'][0]['square_id'] = $square_product_data['object']['item_variation_data']['id']; 796 } 797 } 798 799 $updated_response = $square_helper->update_square_product($woo_data, $square_product_data['object'], $data_to_import); 800 return $updated_response; 801 } else { 802 return $square_product_data; // Return the original data if 'object' or 'type' key is not set 803 } 804 } 805 806 public function export_to_square($new_status, $old_status, $post) 807 { 808 // Ensure WooCommerce is loaded 809 if (!class_exists('WooCommerce')) { 810 error_log('WooCommerce not loaded.'); 811 return; 812 } 813 814 // Check if the post type is 'product' and the new status is 'publish' 815 if ($post->post_type === 'product' && $new_status === 'publish') { 816 // Get the post's published date 817 $published_date = get_the_date('Y-m-d', $post); 818 819 // Get the current date 820 $current_date = current_time('Y-m-d'); 821 822 // If the post has not been previously published (published date is current date) 823 if ($published_date == $current_date) { 824 825 // Get current settings to check if auto-create is enabled 826 $current_settings = get_option('square-woo-sync_settings', []); 827 if (empty($current_settings) || empty($current_settings['wooAuto']['autoCreateProduct']) || !$current_settings['wooAuto']['autoCreateProduct']) { 828 return; 829 } 830 831 // Fetch the product 832 $product_id = $post->ID; 833 $product = wc_get_product($product_id); 834 $square_product_id = get_post_meta($product_id, 'square_product_id', true); 835 836 if (!$product || $square_product_id) { 837 return; 838 } 839 840 $logger = new Logger(); // Ensure this Logger class is defined 841 $uniqueProcessId = wp_generate_uuid4(); 842 843 // Assuming WooImport is a class responsible for handling the export 844 $exporter = new WooImport(); // Ensure this class is defined or included 845 846 // Export the product 847 $result = $exporter->import_products([$product], 1, $uniqueProcessId); 848 $inventoryResult = $exporter->update_square_inventory_counts([$product]); 849 850 // Handle the export result 851 if (is_wp_error($result)) { 852 error_log('Error exporting product to Square'); 853 } else { 854 $logger->log('success', 'Product exported and linked to Square: ' . $product->get_name(), array('process_id' => $uniqueProcessId)); 855 856 set_transient('square_sync_success', 'Product "' . $product->get_name() . '" was successfully created in Square and linked for automatic syncing (if enabled).', 30); 857 } 858 } 859 } 860 } 177 } 861 178 } 862 863 // Display the WooCommerce notice864 add_action('admin_notices', function () {865 if ($message = get_transient('square_sync_success')) {866 echo '<div class="notice notice-success is-dismissible">';867 echo '<p>' . esc_html($message) . '</p>';868 echo '</div>';869 delete_transient('square_sync_success');870 }871 }); -
squarewoosync/trunk/languages/square-woo-sync.pot
r3220600 r3225869 2 2 msgid "" 3 3 msgstr "" 4 "Project-Id-Version: Square Sync for Woocommerce 5.1. 0\n"4 "Project-Id-Version: Square Sync for Woocommerce 5.1.1\n" 5 5 "Report-Msgid-Bugs-To: https://github.com/LiamHillier/square-woo-sync/issues\n" 6 6 "Last-Translator: liam@pixeldev.com.au\n" … … 9 9 "Content-Type: text/plain; charset=UTF-8\n" 10 10 "Content-Transfer-Encoding: 8bit\n" 11 "POT-Creation-Date: 2025-01- 11T18:09:29+11:00\n"11 "POT-Creation-Date: 2025-01-21T12:49:08+11:00\n" 12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 13 "X-Generator: WP-CLI 2.10.0\n" … … 60 60 61 61 #: includes/Admin/Menu.php:50 62 #: squarewoosync.php:21 162 #: squarewoosync.php:212 63 63 msgid "Settings" 64 64 msgstr "" … … 102 102 103 103 #: includes/REST/OrdersController.php:99 104 #: includes/REST/OrdersController.php:1 09104 #: includes/REST/OrdersController.php:112 105 105 msgid "Square API error: " 106 106 msgstr "" 107 107 108 #: includes/REST/OrdersController.php:1 07108 #: includes/REST/OrdersController.php:110 109 109 msgid "Square Payment API error: " 110 110 msgstr "" 111 111 112 #: includes/REST/OrdersController.php:12 5112 #: includes/REST/OrdersController.php:128 113 113 msgid "Order and Transaction created in Square, receipt: #" 114 114 msgstr "" 115 115 116 #: includes/REST/OrdersController.php:14 5116 #: includes/REST/OrdersController.php:148 117 117 msgid "Order created in Square" 118 118 msgstr "" 119 119 120 #: includes/REST/OrdersController.php:15 2120 #: includes/REST/OrdersController.php:155 121 121 msgid "Failed to create order: " 122 122 msgstr "" 123 123 124 #: includes/REST/OrdersController.php:9 59124 #: includes/REST/OrdersController.php:962 125 125 msgid "Woocommerce not installed or activated" 126 126 msgstr "" … … 175 175 msgstr "" 176 176 177 #: squarewoosync.php:21 2177 #: squarewoosync.php:213 178 178 msgid "Documentation" 179 179 msgstr "" 180 180 181 #: squarewoosync.php:21 3181 #: squarewoosync.php:214 182 182 msgid "Go Pro" 183 183 msgstr "" 184 184 185 #: squarewoosync.php:25 3185 #: squarewoosync.php:254 186 186 msgid "Square Product ID" 187 187 msgstr "" 188 188 189 #: squarewoosync.php:25 5189 #: squarewoosync.php:256 190 190 msgid "Enter the Square product ID here." 191 191 msgstr "" -
squarewoosync/trunk/readme.txt
r3220600 r3225869 6 6 Tested up to: 6.7 7 7 Requires PHP: 7.4 8 Stable tag: 5.1. 08 Stable tag: 5.1.1 9 9 License: GPLv2 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 110 110 111 111 == Changelog == 112 = 5.1.1 = 113 * Fix auto order sync for non Square-Sync gateways 114 112 115 = 5.1.0 = 113 116 * Add woocommerce subscriptions support -
squarewoosync/trunk/squarewoosync.php
r3220600 r3225869 12 12 * License URI: http://www.gnu.org/licenses/gpl-2.0.html 13 13 * Domain Path: /languages 14 * Version: 5.1. 014 * Version: 5.1.1 15 15 * Requires at least: 5.4 16 16 * Requires PHP: 7.4 … … 29 29 final class SquareWooSync 30 30 { 31 const VERSION = '5. 0.7';31 const VERSION = '5.1.1'; 32 32 const SLUG = 'squarewoosync'; 33 33 … … 144 144 $this->container['assets'] = new Pixeldev\SquareWooSync\Assets\Manager(); 145 145 $this->container['rest_api'] = new Pixeldev\SquareWooSync\REST\Api(); 146 $this->container['square'] = new Pixeldev\SquareWooSync\Woo\SyncProduct(); 146 147 } 147 148 -
squarewoosync/trunk/vendor/composer/installed.php
r3220600 r3225869 4 4 'pretty_version' => 'dev-main', 5 5 'version' => 'dev-main', 6 'reference' => '8 a016ff1c7c1fe4a3354a6a50d0d282ed6566aa2',6 'reference' => '862e4e7b346b246b0cab176f1b96c60614a4e3ba', 7 7 'type' => 'project', 8 8 'install_path' => __DIR__ . '/../../', … … 14 14 'pretty_version' => 'dev-main', 15 15 'version' => 'dev-main', 16 'reference' => '8 a016ff1c7c1fe4a3354a6a50d0d282ed6566aa2',16 'reference' => '862e4e7b346b246b0cab176f1b96c60614a4e3ba', 17 17 'type' => 'project', 18 18 'install_path' => __DIR__ . '/../../',
Note: See TracChangeset
for help on using the changeset viewer.