Plugin Directory

Changeset 3368696


Ignore:
Timestamp:
09/26/2025 08:31:47 PM (5 months ago)
Author:
loystarapp
Message:

Update to version 3.2.6: Fix duplicate transaction prevention and amount precision formatting

Location:
loystar-woocommerce-loyalty-program/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • loystar-woocommerce-loyalty-program/trunk/changelog.txt

    r3359375 r3368696  
    11== 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
    219
    320= 3.2.2 =
  • loystar-woocommerce-loyalty-program/trunk/includes/api/class-wc-ls-api.php

    r3359375 r3368696  
    1212     * @var string
    1313     */
    14     protected $base_url = 'https://api.loystar.co/api/v2/';
     14    protected $base_url = 'https://api0.loystar.co/api/v2/';
    1515
    1616    /**
     
    9999        // Force production environment
    100100        $this->_env = 'production';
    101         $this->base_url = 'https://api.loystar.co/api/v2/';
     101        $this->base_url = 'https://api0.loystar.co/api/v2/';
    102102
    103103        // Initialize access token property
     
    231231        // Return original if too short or invalid
    232232        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';
    233795    }
    234796
     
    4781040        $m_id = update_option($wc_ls_option_meta['m_id'],$r_data['data']['id'],false);
    4791041        $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       
    4801063        $sub = $this->get_merchant_subscription();
    4811064        $sub_expire = update_option($wc_ls_option_meta['sub_expires'],$sub['expires_on'],false);
     
    5051088        }*/
    5061089       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;
    5071188    }
    5081189
     
    8831564        }
    8841565        $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");
    8871575        //continue :)
    8881576        $branch = wc_loystar()->get_site_branch();
     
    11161804            //quantity of item
    11171805            $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');
    11201808            //product type
    11211809            $transaction_data_array['product_type'] = "product";
     
    11671855        //all went through
    11681856        $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       
    11691862        //add others, this isn't necessary, just to keep record
    11701863        foreach($r_data as $key=>$value){
  • loystar-woocommerce-loyalty-program/trunk/loystar.php

    r3359375 r3368696  
    66 * Author: Loystar Solutions
    77 * Author URI: http://loystar.co
    8  * Version: 3.2.2
     8 * Version: 3.2.6
    99 * Requires at least: 5.0.0
    1010 * Tested up to: 6.8.0
     
    2222define( 'WC_LS_PLUGIN_FILE', __FILE__ );
    2323define( 'WC_LS_TEXT_DOMAIN', 'loystar-woocommerce-loyalty-program' );
    24 define( 'WC_LS_PLUGIN_VERSION', '3.2.2' );
     24define( 'WC_LS_PLUGIN_VERSION', '3.2.6' );
    2525define( 'WC_LS_CLASS_FILE_PREFIX', 'class-wc-ls-' );
    2626
     
    4949    'extra_meta'                => 'wc_loystar_extra_meta', // Incase we need some extras.
    5050    '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
    5154);
    5255
  • loystar-woocommerce-loyalty-program/trunk/readme.txt

    r3359375 r3368696  
    55Tested up to: 6.8.0
    66Requires PHP: 7.4
    7 Stable tag: 3.2.2
     7Stable tag: 3.2.6
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    114114== Changelog ==
    115115
     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
    116133= 3.2.2 =
    117134* Patch: Minor enhancements and optimizations
Note: See TracChangeset for help on using the changeset viewer.