Changeset 3368696
- Timestamp:
- 09/26/2025 08:31:47 PM (5 months ago)
- Location:
- loystar-woocommerce-loyalty-program/trunk
- Files:
-
- 4 edited
-
changelog.txt (modified) (1 diff)
-
includes/api/class-wc-ls-api.php (modified) (8 diffs)
-
loystar.php (modified) (3 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
loystar-woocommerce-loyalty-program/trunk/changelog.txt
r3359375 r3368696 1 1 == Changelog == 2 3 = 3.2.6 = 4 * Fixed: Amount precision issue - order totals now preserve exact decimal values instead of rounding 5 * Fixed: Sales transaction duplicate prevention - prevents duplicate transaction posts 6 * Enhanced: GraphQL order totals and line item amounts maintain decimal precision (e.g., $694.95 stays $694.95, not $695) 7 * Enhanced: Sales transaction API duplicate prevention with success-only marking 8 * Improved: Consistent decimal handling across both GraphQL and REST API endpoints 9 10 = 3.2.3 = 11 * MAJOR: Added GraphQL MTier integration for real-time order synchronization 12 * NEW: Orders now automatically sent to Loystar when placed (processing/on-hold status) 13 * NEW: Order status updates sent via GraphQL when completed (prevents duplicates) 14 * NEW: GraphQL configuration settings in admin panel with JWT token support 15 * Enhanced: Smart duplicate prevention - orders sent via GraphQL are updated, not recreated 16 * Enhanced: Comprehensive order data including delivery addresses and order notes 17 * Enhanced: Fallback to REST API for orders not initially sent via GraphQL 18 * Improved: Real-time order integration replaces delayed completion-only sync 2 19 3 20 = 3.2.2 = -
loystar-woocommerce-loyalty-program/trunk/includes/api/class-wc-ls-api.php
r3359375 r3368696 12 12 * @var string 13 13 */ 14 protected $base_url = 'https://api .loystar.co/api/v2/';14 protected $base_url = 'https://api0.loystar.co/api/v2/'; 15 15 16 16 /** … … 99 99 // Force production environment 100 100 $this->_env = 'production'; 101 $this->base_url = 'https://api .loystar.co/api/v2/';101 $this->base_url = 'https://api0.loystar.co/api/v2/'; 102 102 103 103 // Initialize access token property … … 231 231 // Return original if too short or invalid 232 232 return $phone; 233 } 234 235 /** 236 * Send order to Loystar MTier GraphQL endpoint 237 * 238 * @param WC_Order|int $order 239 * @param string $status Order status for Loystar 240 * @param bool $force_resend Force re-sending even if already sent 241 * @return bool|array Returns order data on success, false on failure 242 */ 243 public function send_order_to_loystar_graphql($order, $status = 'pending', $force_resend = false) { 244 $this->clear_transaction_error(); 245 246 if (is_int($order)) { 247 try { 248 $order = new WC_Order($order); 249 } catch (Exception $ex) { 250 $this->set_transaction_error_prop('Invalid order ID', 1); 251 return false; 252 } 253 } 254 255 $order_id = $order->get_id(); 256 257 // Check if order is already sent (duplicate prevention) 258 $loystar_order_sent = get_post_meta($order_id, '_loystar_order_sent', true); 259 if ($loystar_order_sent === 'yes' && !$force_resend) { 260 error_log("Loystar GraphQL: Order {$order_id} already sent, skipping duplicate send"); 261 $this->set_transaction_error_prop('Order already sent to Loystar', 1); 262 return false; 263 } 264 265 if ($force_resend) { 266 error_log("Loystar GraphQL: Force re-sending order {$order_id} with status: {$status}"); 267 } 268 269 error_log("Loystar GraphQL: Starting order {$order_id} sync with status: {$status}"); 270 271 // Get GraphQL endpoint and credentials 272 global $wc_ls_option_meta; 273 $graphql_url = 'https://mtier0.loystar.co/graphql'; // Constant URL for all merchants 274 $jwt_token = get_option($wc_ls_option_meta['mtier_jwt_token']); 275 $merchant_id = (int)get_option($wc_ls_option_meta['m_id']); 276 277 // Fallback: Use access token if JWT token not available 278 if (empty($jwt_token)) { 279 $jwt_token = get_option($wc_ls_option_meta['access_token']); 280 error_log('Loystar GraphQL: Using access token as JWT fallback'); 281 } 282 283 if (empty($jwt_token) || empty($merchant_id)) { 284 $this->set_transaction_error_prop('Missing authentication token or merchant ID', 1); 285 return false; 286 } 287 288 // Prepare order data for GraphQL 289 $order_data = $this->prepare_loystar_order_data($order, $merchant_id, $status); 290 if (!$order_data) { 291 return false; 292 } 293 294 // Store the UUID we're sending to Loystar (needed for payOneOrder later) 295 $loystar_uuid = $order_data['orderID']; 296 update_post_meta($order_id, '_loystar_order_uuid', $loystar_uuid); 297 298 // GraphQL mutation - MUST match working example exactly 299 $mutation = 'mutation AddOrders($data: order!) { 300 addOrders(data: $data) { 301 order_id 302 customer { 303 user_id 304 __typename 305 } 306 status 307 __typename 308 } 309 }'; 310 311 $payload = [ 312 'operationName' => 'AddOrders', // CRITICAL: Must include operationName 313 'query' => $mutation, 314 'variables' => [ 315 'data' => $order_data 316 ] 317 ]; 318 319 // COMPREHENSIVE LOGGING: Log the complete request payload for debugging 320 error_log("=== LOYSTAR GRAPHQL REQUEST DEBUG - ORDER {$order_id} ==="); 321 error_log("Endpoint: " . $graphql_url); 322 error_log("JWT Token (first 20 chars): " . substr($jwt_token, 0, 20) . "..."); 323 error_log("Complete Request Payload:"); 324 error_log(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 325 error_log("Order Data Structure:"); 326 error_log(json_encode($order_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 327 error_log("Order Items Count: " . count($order_data['order_items'])); 328 foreach ($order_data['order_items'] as $index => $item) { 329 error_log("Item " . ($index + 1) . ": " . $item['name'] . " - total_quantity: " . $item['total_quantity'] . ", bundle_quantity: " . $item['bundle_quantity'] . ", track_inventory: " . ($item['track_inventory'] ? 'true' : 'false')); 330 } 331 error_log("=== END REQUEST DEBUG ==="); 332 333 // Make GraphQL request 334 $response = wp_remote_post($graphql_url, [ 335 'headers' => [ 336 'Content-Type' => 'application/json', 337 'Authorization' => 'Bearer ' . $jwt_token 338 ], 339 'body' => json_encode($payload), 340 'timeout' => 30 341 ]); 342 343 if (is_wp_error($response)) { 344 $this->set_transaction_error_prop('GraphQL request failed: ' . $response->get_error_message(), -1); 345 $this->add_log($order_id, 'graphql_request_error', $response->get_error_message()); 346 return false; 347 } 348 349 $response_body = wp_remote_retrieve_body($response); 350 $response_data = json_decode($response_body, true); 351 $http_status = wp_remote_retrieve_response_code($response); 352 353 // COMPREHENSIVE LOGGING: Log the complete response for debugging 354 error_log("=== LOYSTAR GRAPHQL RESPONSE DEBUG - ORDER {$order_id} ==="); 355 error_log("HTTP Status: " . $http_status); 356 error_log("Raw Response Body:"); 357 error_log($response_body); 358 error_log("Parsed Response Data:"); 359 error_log(json_encode($response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 360 error_log("=== END RESPONSE DEBUG ==="); 361 362 if (isset($response_data['errors'])) { 363 error_log("LOYSTAR ERROR DETECTED - ORDER {$order_id}: " . json_encode($response_data['errors'])); 364 $error_message = $response_data['errors'][0]['message'] ?? 'Unknown GraphQL error'; 365 $this->set_transaction_error_prop('GraphQL error: ' . $error_message, 1); 366 $this->add_log($order_id, 'graphql_error', $error_message); 367 return false; 368 } 369 370 if (isset($response_data['data']['addOrders'])) { 371 $loystar_order = $response_data['data']['addOrders']; 372 373 // Store Loystar order ID in meta for future updates 374 // The working GraphQL response returns order_id instead of id 375 $loystar_order_id = $loystar_order['order_id'] ?? $loystar_order['id'] ?? null; 376 377 if ($loystar_order_id) { 378 update_post_meta($order_id, '_loystar_order_id', $loystar_order_id); 379 update_post_meta($order_id, '_loystar_order_sent', 'yes'); 380 update_post_meta($order_id, '_loystar_order_status', $status); 381 382 $this->add_log($order_id, 'graphql_order_success', 'Order sent to Loystar successfully: ' . $loystar_order_id); 383 return $loystar_order; 384 } else { 385 $this->set_transaction_error_prop('No order ID in GraphQL response', -1); 386 return false; 387 } 388 } 389 390 $this->set_transaction_error_prop('Unexpected GraphQL response format', -1); 391 return false; 392 } 393 394 /** 395 * Mark order as paid in Loystar using payOneOrder mutation 396 * 397 * @param int $order_id 398 * @return bool 399 */ 400 public function mark_loystar_order_as_paid($order_id) { 401 // Use the UUID we generated and sent to Loystar, not the numeric ID they returned 402 $loystar_order_uuid = get_post_meta($order_id, '_loystar_order_uuid', true); 403 404 if (empty($loystar_order_uuid)) { 405 $this->set_transaction_error_prop('No Loystar order UUID found for update', 1); 406 return false; 407 } 408 409 // Get GraphQL endpoint and credentials 410 global $wc_ls_option_meta; 411 $graphql_url = 'https://mtier0.loystar.co/graphql'; // Constant URL for all merchants 412 $jwt_token = get_option($wc_ls_option_meta['mtier_jwt_token']); 413 414 // Fallback: Use access token if JWT token not available 415 if (empty($jwt_token)) { 416 $jwt_token = get_option($wc_ls_option_meta['access_token']); 417 error_log('Loystar GraphQL Update: Using access token as JWT fallback'); 418 } 419 420 if (empty($jwt_token)) { 421 $this->set_transaction_error_prop('Missing authentication token', 1); 422 return false; 423 } 424 425 // Use the correct payOneOrder query to mark order as paid 426 $query = 'query payOneOrder($id: String!) { 427 payOneOrder(id: $id) { 428 id 429 order_id 430 merchant_id 431 customer { 432 first_name 433 last_name 434 user_id 435 phone_number 436 email 437 delivery_address 438 __typename 439 } 440 orderNotes 441 total 442 order_type 443 processed_by 444 reference_code 445 created_at 446 status 447 paid_at 448 paid_time 449 updated_at 450 expiry_date 451 __typename 452 } 453 }'; 454 455 $payload = [ 456 'operationName' => 'payOneOrder', 457 'query' => $query, 458 'variables' => [ 459 'id' => $loystar_order_uuid 460 ] 461 ]; 462 463 // Make GraphQL request 464 $response = wp_remote_post($graphql_url, [ 465 'headers' => [ 466 'Content-Type' => 'application/json', 467 'Authorization' => 'Bearer ' . $jwt_token 468 ], 469 'body' => json_encode($payload), 470 'timeout' => 30 471 ]); 472 473 if (is_wp_error($response)) { 474 $this->set_transaction_error_prop('GraphQL update request failed: ' . $response->get_error_message(), -1); 475 $this->add_log($order_id, 'graphql_update_error', $response->get_error_message()); 476 return false; 477 } 478 479 $response_body = wp_remote_retrieve_body($response); 480 $response_data = json_decode($response_body, true); 481 482 // Debug: Log the response 483 error_log("=== LOYSTAR payOneOrder QUERY DEBUG - ORDER {$order_id} ==="); 484 error_log("UUID used: " . $loystar_order_uuid); 485 error_log("Response: " . $response_body); 486 error_log("Parsed Data: " . print_r($response_data, true)); 487 error_log("=== END payOneOrder DEBUG ==="); 488 489 if (isset($response_data['errors'])) { 490 $error_message = $response_data['errors'][0]['message'] ?? 'Unknown GraphQL payment error'; 491 $this->set_transaction_error_prop('GraphQL payment error: ' . $error_message, 1); 492 $this->add_log($order_id, 'graphql_payment_error', $error_message); 493 return false; 494 } 495 496 if (isset($response_data['data']['payOneOrder'])) { 497 $order_data = $response_data['data']['payOneOrder']; 498 // Update local meta with paid status 499 update_post_meta($order_id, '_loystar_order_status', 'paid'); 500 $this->add_log($order_id, 'graphql_payment_success', 'Order payment processed in Loystar: ' . $loystar_order_uuid); 501 return true; 502 } 503 504 $this->set_transaction_error_prop('Unexpected GraphQL payment response format: ' . $response_body, -1); 505 return false; 506 } 507 508 /** 509 * Prepare order data for Loystar GraphQL mutation 510 * 511 * @param WC_Order $order 512 * @param int $merchant_id 513 * @param string $status 514 * @return array|false 515 */ 516 private function prepare_loystar_order_data($order, $merchant_id, $status = 'pending') { 517 $order_id = $order->get_id(); 518 519 // Get customer data using proper methods 520 $fname = $order->get_billing_first_name(); 521 $lname = $order->get_billing_last_name(); 522 $email = $order->get_billing_email(); 523 $phone = $order->get_billing_phone(); 524 525 // Get custom fields 526 global $wc_ls_woo_custom_field_meta; 527 $dob = trim(wc_loystar()->get_woo_order_meta($order_id, '_'.$wc_ls_woo_custom_field_meta['billing_dob'], true)); 528 $sex = strtoupper(trim(wc_loystar()->get_woo_order_meta($order_id, '_'.$wc_ls_woo_custom_field_meta['billing_gender'], true))); 529 530 // Normalize phone and set defaults 531 if (empty($phone)) { 532 $phone = '+234080' . rand(1000000, 9999999); 533 } else { 534 $phone = $this->normalize_phone_number($phone); 535 } 536 537 if (empty($sex) || !in_array($sex, ['M', 'F'])) { 538 $sex = ''; // Leave empty for GraphQL 539 } 540 541 // Get delivery address 542 $use_shipping = !empty($order->get_shipping_address_1()) || !empty($order->get_shipping_address_2()); 543 544 if ($use_shipping) { 545 $delivery_address = trim(implode(', ', array_filter([ 546 $order->get_shipping_address_1(), 547 $order->get_shipping_address_2(), 548 $order->get_shipping_city(), 549 $order->get_shipping_state(), 550 $order->get_shipping_postcode(), 551 $order->get_shipping_country() 552 ]))); 553 } else { 554 $delivery_address = trim(implode(', ', array_filter([ 555 $order->get_billing_address_1(), 556 $order->get_billing_address_2(), 557 $order->get_billing_city(), 558 $order->get_billing_state(), 559 $order->get_billing_postcode(), 560 $order->get_billing_country() 561 ]))); 562 } 563 564 // Prepare customer data - MUST match working example structure 565 $customer_data = [ 566 'user_id' => $order->get_customer_id() ?: 0, 567 'first_name' => $fname, 568 'last_name' => $lname, 569 'phone_number' => $phone, 570 'email' => $email, 571 'sex' => !empty($sex) ? $sex : '-', // Use '-' as default like working example 572 'address' => '', // Required field in working example 573 'delivery_address' => $delivery_address 574 ]; 575 // Allow integrators to adjust any customer fields (avoid hardcoding assumptions) 576 $customer_data = apply_filters('wc_ls_loystar_customer_data', $customer_data, $order); 577 578 // Prepare order items 579 $order_items = []; 580 $order_number = (string) $order->get_order_number(); // Get the order number for item association 581 582 foreach ($order->get_items() as $item_id => $item) { 583 $product = $item->get_product(); 584 585 if (!$product) { 586 continue; 587 } 588 589 // Get Loystar equivalent product ID 590 $wc_product_id = $product->get_id(); 591 $loystar_product_id = wc_loystar()->get_equiv_loystar_product($wc_product_id); 592 593 // Skip items that don't have a Loystar product mapping 594 if (empty($loystar_product_id)) { 595 $this->add_log($order->get_id(), 'product_sync_warning', 596 "Product {$wc_product_id} ({$item->get_name()}) not synced with Loystar - skipping from order"); 597 continue; 598 } 599 600 // Calculate unit price and total amount correctly 601 $unit_price = (float) ($item->get_subtotal() / $item->get_quantity()); 602 $total_amount = (float) $item->get_total(); 603 604 // Get product variants if applicable 605 $variants = []; 606 if ($product->is_type('variation')) { 607 $variation_attributes = $product->get_variation_attributes(); 608 foreach ($variation_attributes as $attribute_name => $attribute_value) { 609 $variants[] = [ 610 'name' => wc_attribute_label($attribute_name), 611 'value' => $attribute_value, 612 'price' => '0.00' 613 ]; 614 } 615 } 616 617 // Derive optional category mapping (avoid hard-coded values) 618 $merchant_product_category_id = null; 619 $mapped_cat = get_post_meta($wc_product_id, '_loystar_product_category_id', true); 620 if (!empty($mapped_cat)) { 621 $merchant_product_category_id = (int) $mapped_cat; 622 } 623 // Allow override via filter if your store maps Woo -> Loystar categories elsewhere 624 $merchant_product_category_id = apply_filters( 625 'wc_ls_loystar_product_category_id', 626 $merchant_product_category_id, 627 $product, 628 $order 629 ); 630 631 // Structure must match working example exactly 632 $track_inventory = apply_filters('wc_ls_loystar_track_inventory', true, $product, $order); 633 $product_type = apply_filters('wc_ls_loystar_product_type', 'product', $product, $order); 634 635 $item_data = [ 636 // IMPORTANT: Use the actual merchant ID for this tenant 637 'merchant_id' => (int) $merchant_id, 638 'name' => $item->get_name(), 639 'picture' => wp_get_attachment_image_url($product->get_image_id(), 'full') ?: null, 640 'quantity' => $item->get_quantity(), 641 'price' => (string) number_format($unit_price, 1, '.', ''), // Format like "44.0" 642 'id' => (int) $loystar_product_id, // This is the internal Loystar ID 643 'service_id' => null, 644 'service_name' => null, 645 'description' => $product->get_short_description() ?: null, 646 'product_type' => $product_type, 647 'product_id' => (int) $loystar_product_id, // Product association field 648 'amount' => $total_amount, // Preserve decimal precision for accurate amounts 649 'total_quantity' => (int) $item->get_quantity(), // CRITICAL FIX: Should be QUANTITY not amount! 650 'tax' => null, 651 'tax_rate' => null, 652 'tax_type' => null, 653 'track_inventory' => (bool) $track_inventory, // true by default, but filterable 654 'merchant_loyalty_program_id' => null, 655 'merchant_product_category_id' => $merchant_product_category_id !== null ? (int) $merchant_product_category_id : null, 656 'variants' => [], 657 'custom_quantities' => [], 658 'bundle_quantity' => (string) number_format($item->get_quantity(), 1, '.', ''), // CRITICAL FIX: Should be QUANTITY not amount! 659 'bundle_id' => null, 660 'weight' => null 661 ]; 662 // Allow integrators to adjust any item fields (avoid hardcoding assumptions) 663 $item_data = apply_filters('wc_ls_loystar_order_item_data', $item_data, $item, $product, $order); 664 $order_items[] = $item_data; 665 } 666 667 // Get merchant settings 668 global $wc_ls_option_meta; 669 $merchant_phone = get_option($wc_ls_option_meta['merchant_phone'], ''); 670 $merchant_name = get_option('blogname', ''); 671 $merchant_email = get_option('admin_email', ''); 672 673 // Prepare complete order data - MUST match working example structure 674 $created_date = $order->get_date_created(); 675 $expiry_date = clone $created_date; 676 $expiry_date->modify('+3 days'); // Add 3 days like working example 677 678 // Allow integrators to override order_type; default to 'in-store' to match working example 679 $order_type = apply_filters('wc_ls_loystar_order_type', 'in-store', $order); 680 // Allow integrators to override processed_by; default to 'woocommerce' as requested 681 $processed_by = apply_filters('wc_ls_loystar_processed_by', 'woocommerce', $order); 682 683 $order_data = [ 684 'orderID' => wp_generate_uuid4(), 685 'order_id' => (string) $order->get_order_number(), 686 'merchant_id' => $merchant_id, 687 'merchant_phone_number' => $merchant_phone, 688 'merchant_email' => $merchant_email, 689 'merchant_name' => $merchant_name, 690 'orderNotes' => $order->get_customer_note() ?: '', 691 'customer' => $customer_data, 692 'order_items' => $order_items, 693 'created_at' => $created_date->format('c'), // ISO format with timezone like working example 694 'status' => $status, 695 'processed_by' => $processed_by, 696 'reference_code' => $order->get_transaction_id() ?: '', 697 'order_type' => $order_type, 698 'paid_at' => $order->get_date_paid() ? $order->get_date_paid()->format('c') : null, 699 'paid_time' => $order->get_date_paid() ? $order->get_date_paid()->format('c') : null, 700 'expiry_date' => $expiry_date->format('c'), // Ensure this is not overwritten later 701 // Total should preserve decimal precision to match exact order amount 702 'total' => (float) $order->get_total() 703 ]; 704 // Allow integrators to adjust any order-level fields (avoid hardcoding assumptions) 705 $order_data = apply_filters('wc_ls_loystar_order_data', $order_data, $order); 706 707 // Critical validation: Ensure proper order data 708 if (empty($order_data['orderID']) || empty($order_data['order_id'])) { 709 $this->add_log($order->get_id(), 'order_validation_error', 'Order ID is required for GraphQL submission'); 710 return false; 711 } 712 713 // Validate that all order items have required fields 714 foreach ($order_items as $item) { 715 if (empty($item['product_id']) || empty($item['name'])) { 716 $this->add_log($order->get_id(), 'item_validation_error', 717 "Order item validation failed - missing product_id or name for item: " . ($item['name'] ?? 'unknown')); 718 return false; 719 } 720 } 721 722 // Log the order structure for debugging 723 error_log("Loystar Order Data: Order ID = {$order_data['order_id']}, Items = " . count($order_items) . ", Total = {$order_data['total']}"); 724 725 return $order_data; 726 } 727 728 /** 729 * Map WooCommerce status to Loystar status 730 * 731 * @param string $woo_status 732 * @param WC_Order $order (optional) - Used to check payment status 733 * @return string 734 */ 735 public function map_woo_status_to_loystar($woo_status, $order = null) { 736 $status_map = [ 737 'pending' => 'pending', 738 'processing' => 'processing', 739 'on-hold' => 'pending', 740 'completed' => 'paid', 741 'cancelled' => 'cancelled', 742 'refunded' => 'cancelled', 743 'failed' => 'cancelled' 744 ]; 745 746 $mapped_status = $status_map[$woo_status] ?? 'pending'; 747 748 // If order is provided and has been paid electronically, always use "paid" status 749 if ($order && $this->is_order_paid_electronically($order)) { 750 $mapped_status = 'paid'; 751 error_log("Loystar Status Mapping: Order {$order->get_id()} paid electronically - status overridden to 'paid'"); 752 } 753 754 return $mapped_status; 755 } 756 757 /** 758 * Check if order was paid electronically (not cash/manual) 759 * 760 * @param WC_Order $order 761 * @return bool 762 */ 763 public function is_order_paid_electronically($order) { 764 // Check if order has a paid date (indicates electronic payment) 765 $date_paid = $order->get_date_paid(); 766 if (!$date_paid) { 767 return false; 768 } 769 770 // Check payment method - electronic payment methods 771 $payment_method = $order->get_payment_method(); 772 $electronic_payment_methods = [ 773 'stripe', 'paypal', 'square', 'authorize_net', 'braintree', 774 'razorpay', 'paystack', 'flutterwave', 'bacs', 'cheque', 775 'cod' // Exclude cash on delivery 776 ]; 777 778 // If it's COD (cash on delivery), it's not electronic 779 if ($payment_method === 'cod') { 780 return false; 781 } 782 783 // If order is marked as paid and has a transaction ID, consider it electronic 784 if ($order->get_transaction_id() && $date_paid) { 785 return true; 786 } 787 788 // If payment method is in electronic list and order is paid 789 if (in_array($payment_method, $electronic_payment_methods) && $date_paid) { 790 return true; 791 } 792 793 // Default: if has paid date and not COD, likely electronic 794 return $date_paid !== null && $payment_method !== 'cod'; 233 795 } 234 796 … … 478 1040 $m_id = update_option($wc_ls_option_meta['m_id'],$r_data['data']['id'],false); 479 1041 $expire = update_option($wc_ls_option_meta['expiring'],$headers['expiry'],false); 1042 1043 // Extract phone from login response if available (JWT will come from MTier) 1044 $phone = null; 1045 if (isset($r_data['data']['phone'])) { 1046 $phone = $r_data['data']['phone']; 1047 } elseif (isset($r_data['data']['mobile'])) { 1048 $phone = $r_data['data']['mobile']; 1049 } elseif (isset($r_data['data']['phone_number'])) { 1050 $phone = $r_data['data']['phone_number']; 1051 } elseif (isset($r_data['data']['mobile_number'])) { 1052 $phone = $r_data['data']['mobile_number']; 1053 } 1054 1055 if ($phone) { 1056 update_option($wc_ls_option_meta['merchant_phone'], $phone, false); 1057 error_log('Loystar: Merchant phone stored from login: ' . $phone); 1058 } 1059 1060 // Get JWT token from MTier authentication 1061 $this->authenticate_with_mtier(trim($email), $password); 1062 480 1063 $sub = $this->get_merchant_subscription(); 481 1064 $sub_expire = update_option($wc_ls_option_meta['sub_expires'],$sub['expires_on'],false); … … 505 1088 }*/ 506 1089 return true; 1090 } 1091 1092 /** 1093 * Authenticate with MTier GraphQL service to get JWT token 1094 * 1095 * @param string $email 1096 * @param string $password 1097 * @return bool 1098 */ 1099 private function authenticate_with_mtier($email, $password) { 1100 global $wc_ls_option_meta; 1101 1102 $mtier_login_url = 'https://mtier0.loystar.co/auth/login'; 1103 1104 $args = array( 1105 'method' => 'POST', 1106 'headers' => array( 1107 'Content-Type' => 'application/json', 1108 'Accept' => 'application/json' 1109 ), 1110 'body' => json_encode(array( 1111 'email' => trim($email), 1112 'password' => $password 1113 )), 1114 'timeout' => 30 1115 ); 1116 1117 error_log('Loystar MTier: Attempting authentication with ' . $mtier_login_url . ' for email: ' . $email); 1118 1119 $response = wp_remote_post($mtier_login_url, $args); 1120 1121 if (is_wp_error($response)) { 1122 error_log('Loystar MTier: Authentication failed - ' . $response->get_error_message()); 1123 return false; 1124 } 1125 1126 $response_code = wp_remote_retrieve_response_code($response); 1127 $response_body = wp_remote_retrieve_body($response); 1128 $response_headers = wp_remote_retrieve_headers($response); 1129 $response_data = json_decode($response_body, true); 1130 1131 error_log('Loystar MTier: Response code: ' . $response_code); 1132 error_log('Loystar MTier: Response headers: ' . print_r($response_headers, true)); 1133 error_log('Loystar MTier: Response body: ' . $response_body); 1134 1135 if ($response_code === 200) { 1136 // Try different possible token locations, starting with the correct one 1137 $jwt_token = null; 1138 if (isset($response_data['data']['mtierToken'])) { 1139 $jwt_token = $response_data['data']['mtierToken']; 1140 error_log('Loystar MTier: Found token in response[data][mtierToken]'); 1141 } elseif (isset($response_data['token'])) { 1142 $jwt_token = $response_data['token']; 1143 error_log('Loystar MTier: Found token in response[token]'); 1144 } elseif (isset($response_data['access_token'])) { 1145 $jwt_token = $response_data['access_token']; 1146 error_log('Loystar MTier: Found token in response[access_token]'); 1147 } elseif (isset($response_data['data']['token'])) { 1148 $jwt_token = $response_data['data']['token']; 1149 error_log('Loystar MTier: Found token in response[data][token]'); 1150 } elseif (isset($response_headers['Authorization'])) { 1151 $jwt_token = str_replace('Bearer ', '', $response_headers['Authorization']); 1152 error_log('Loystar MTier: Found token in Authorization header'); 1153 } 1154 1155 if ($jwt_token) { 1156 $token_update = update_option($wc_ls_option_meta['mtier_jwt_token'], $jwt_token, false); 1157 error_log('Loystar MTier: JWT Token obtained and stored: ' . substr($jwt_token, 0, 20) . '... (Update result: ' . ($token_update ? 'success' : 'failed') . ')'); 1158 1159 // Verify token was stored 1160 $stored_token = get_option($wc_ls_option_meta['mtier_jwt_token']); 1161 error_log('Loystar MTier: Verification - stored token: ' . (!empty($stored_token) ? 'Found' : 'Not found')); 1162 1163 // Also extract phone if available in MTier response 1164 if (isset($response_data['user']['phone'])) { 1165 update_option($wc_ls_option_meta['merchant_phone'], $response_data['user']['phone'], false); 1166 error_log('Loystar MTier: Phone number stored: ' . $response_data['user']['phone']); 1167 } elseif (isset($response_data['phone'])) { 1168 update_option($wc_ls_option_meta['merchant_phone'], $response_data['phone'], false); 1169 error_log('Loystar MTier: Phone number stored: ' . $response_data['phone']); 1170 } 1171 1172 return true; 1173 } else { 1174 error_log('Loystar MTier: No token found in any expected location'); 1175 error_log('Loystar MTier: Full response structure: ' . print_r($response_data, true)); 1176 } 1177 } else { 1178 error_log('Loystar MTier: Authentication failed - Response code: ' . $response_code); 1179 if (isset($response_data['message'])) { 1180 error_log('Loystar MTier: Error message: ' . $response_data['message']); 1181 } 1182 if (isset($response_data['error'])) { 1183 error_log('Loystar MTier: Error details: ' . print_r($response_data['error'], true)); 1184 } 1185 } 1186 1187 return false; 507 1188 } 508 1189 … … 883 1564 } 884 1565 $order_id = $order->ID; 885 //the transaction was attempted to be sent, so update the meta 886 wc_loystar()->update_woo_order_meta($order_id,WC_LS_ORDER_TRANS_SEND_KEY,'1'); 1566 1567 // DUPLICATE PREVENTION: Check if transaction was already sent successfully 1568 $already_sent = wc_loystar()->get_woo_order_meta($order_id, WC_LS_ORDER_TRANS_SEND_KEY); 1569 if ($already_sent == '1') { 1570 error_log("Loystar Sales Transaction: Order {$order_id} transaction already sent - preventing duplicate"); 1571 return true; // Return true as transaction was already successful 1572 } 1573 1574 error_log("Loystar Sales Transaction: Sending order {$order_id} to sales endpoint"); 887 1575 //continue :) 888 1576 $branch = wc_loystar()->get_site_branch(); … … 1116 1804 //quantity of item 1117 1805 $transaction_data_array['quantity'] = $item_data->get_quantity(); 1118 //timestamp 1119 $transaction_data_array['created_at'] = time();1806 //timestamp - use actual order creation date, not current time 1807 $transaction_data_array['created_at'] = $order->get_date_created()->format('Y-m-d H:i:s'); 1120 1808 //product type 1121 1809 $transaction_data_array['product_type'] = "product"; … … 1167 1855 //all went through 1168 1856 $this->add_log($order_id,'transaction_sent','true'); 1857 1858 // Mark transaction as successfully sent to prevent duplicates 1859 wc_loystar()->update_woo_order_meta($order_id,WC_LS_ORDER_TRANS_SEND_KEY,'1'); 1860 error_log("Loystar Sales Transaction: Order {$order_id} transaction sent successfully - marked to prevent duplicates"); 1861 1169 1862 //add others, this isn't necessary, just to keep record 1170 1863 foreach($r_data as $key=>$value){ -
loystar-woocommerce-loyalty-program/trunk/loystar.php
r3359375 r3368696 6 6 * Author: Loystar Solutions 7 7 * Author URI: http://loystar.co 8 * Version: 3.2. 28 * Version: 3.2.6 9 9 * Requires at least: 5.0.0 10 10 * Tested up to: 6.8.0 … … 22 22 define( 'WC_LS_PLUGIN_FILE', __FILE__ ); 23 23 define( 'WC_LS_TEXT_DOMAIN', 'loystar-woocommerce-loyalty-program' ); 24 define( 'WC_LS_PLUGIN_VERSION', '3.2. 2' );24 define( 'WC_LS_PLUGIN_VERSION', '3.2.6' ); 25 25 define( 'WC_LS_CLASS_FILE_PREFIX', 'class-wc-ls-' ); 26 26 … … 49 49 'extra_meta' => 'wc_loystar_extra_meta', // Incase we need some extras. 50 50 'global_notices' => 'wc_loystar_notices', 51 // GraphQL MTier settings (automatically obtained from login) 52 'mtier_jwt_token' => 'wc_loystar_mtier_jwt_token', // JWT token from login response 53 'merchant_phone' => 'wc_loystar_merchant_phone', // Merchant phone from login response 51 54 ); 52 55 -
loystar-woocommerce-loyalty-program/trunk/readme.txt
r3359375 r3368696 5 5 Tested up to: 6.8.0 6 6 Requires PHP: 7.4 7 Stable tag: 3.2. 27 Stable tag: 3.2.6 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 114 114 == Changelog == 115 115 116 = 3.2.6 = 117 * Fixed: Amount precision issue - order totals now preserve exact decimal values instead of rounding 118 * Fixed: Sales transaction duplicate prevention - prevents duplicate transaction posts 119 * Enhanced: GraphQL order totals and line item amounts maintain decimal precision (e.g., $694.95 stays $694.95, not $695) 120 * Enhanced: Sales transaction API duplicate prevention with success-only marking 121 * Improved: Consistent decimal handling across both GraphQL and REST API endpoints 122 123 = 3.2.3 = 124 * MAJOR: Added GraphQL MTier integration for real-time order synchronization 125 * NEW: Orders now automatically sent to Loystar when placed (processing/on-hold status) 126 * NEW: Order status updates sent via GraphQL when completed (prevents duplicates) 127 * NEW: GraphQL configuration settings in admin panel with JWT token support 128 * Enhanced: Smart duplicate prevention - orders sent via GraphQL are updated, not recreated 129 * Enhanced: Comprehensive order data including delivery addresses and order notes 130 * Enhanced: Fallback to REST API for orders not initially sent via GraphQL 131 * Improved: Real-time order integration replaces delayed completion-only sync 132 116 133 = 3.2.2 = 117 134 * Patch: Minor enhancements and optimizations
Note: See TracChangeset
for help on using the changeset viewer.