From 36f0d52290520beaf247dec224e0b9a9c6e1c506 Mon Sep 17 00:00:00 2001
From: Pavel <pavel.lance@gmail.com>
Date: Mon, 25 Nov 2024 20:27:27 +0200
Subject: [PATCH 1/2] Added Order paid completed webhook

---
 README.txt                                    |   5 +-
 assets/js/admin-javascript.js                 |   7 +-
 bootstrap.php                                 |   1 +
 gtm-server-side.php                           |   3 +-
 includes/class-gtm-server-side-admin-ajax.php | 123 ++++++++++--------
 .../class-gtm-server-side-admin-settings.php  |  28 +++-
 includes/class-gtm-server-side-wc-helpers.php |   8 ++
 ...lass-gtm-server-side-webhook-completed.php |  86 ++++++++++++
 ...ass-gtm-server-side-webhook-processing.php |   2 +-
 .../class-gtm-server-side-webhook-refund.php  |   2 +-
 10 files changed, 205 insertions(+), 60 deletions(-)
 create mode 100644 includes/class-gtm-server-side-webhook-completed.php

diff --git a/README.txt b/README.txt
index dfedf11..69e4718 100644
--- a/README.txt
+++ b/README.txt
@@ -3,7 +3,7 @@ Contributors: gtmserver,bukashk0zzz
 Tags: google tag manager, google tag manager server side, gtm, gtm server side, tag manager, tagmanager, analytics, google, serverside, server-side, gtag
 Requires at least: 5.2.0
 Tested up to: 6.7.0
-Stable tag: 2.1.23
+Stable tag: 2.1.24
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -67,6 +67,9 @@ Yes. <a href="https://stape.io/blog/how-to-set-up-facebook-conversion-api">How t
 
 == Changelog ==
 
+= 2.1.24 =
+* Added Order paid completed webhook
+
 = 2.1.23 =
 * Fix purchase event
 
diff --git a/assets/js/admin-javascript.js b/assets/js/admin-javascript.js
index 871bcaa..115a574 100644
--- a/assets/js/admin-javascript.js
+++ b/assets/js/admin-javascript.js
@@ -54,12 +54,13 @@ jQuery( document ).ready(
 				}
 
 				var isPurchaseChecked   = jQuery( '#gtm_server_side_webhooks_purchase' ).is( ':checked' );
-				var isRefundChecked     = jQuery( '#gtm_server_side_webhooks_refund' ).is( ':checked' );
 				var isProcessingChecked = jQuery( '#gtm_server_side_webhooks_processing' ).is( ':checked' );
+				var isCompletedChecked  = jQuery( '#gtm_server_side_webhooks_completed' ).is( ':checked' );
+				var isRefundChecked     = jQuery( '#gtm_server_side_webhooks_refund' ).is( ':checked' );
 
-				return isPurchaseChecked || isRefundChecked || isProcessingChecked;
+				return isPurchaseChecked || isProcessingChecked || isCompletedChecked || isRefundChecked;
 			},
-			'Select purchase and/or refund webhook'
+			'Select one or more webhooks'
 		);
 
 		// Tab "General".
diff --git a/bootstrap.php b/bootstrap.php
index 965a2c2..a38552b 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -32,6 +32,7 @@
 define( 'GTM_SERVER_SIDE_FIELD_WEBHOOKS_CONTAINER_URL', 'gtm_server_side_webhooks_container_url' );
 define( 'GTM_SERVER_SIDE_FIELD_WEBHOOKS_PURCHASE', 'gtm_server_side_webhooks_purchase' );
 define( 'GTM_SERVER_SIDE_FIELD_WEBHOOKS_PROCESSING', 'gtm_server_side_webhooks_processing' );
+define( 'GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED', 'gtm_server_side_webhooks_completed' );
 define( 'GTM_SERVER_SIDE_FIELD_WEBHOOKS_REFUND', 'gtm_server_side_webhooks_refund' );
 
 define( 'GTM_SERVER_SIDE_FIELD_PLACEMENT_VALUE_CODE', 'code' );
diff --git a/gtm-server-side.php b/gtm-server-side.php
index 7138e2e..6f05819 100644
--- a/gtm-server-side.php
+++ b/gtm-server-side.php
@@ -10,7 +10,7 @@
  * Plugin Name:       GTM Server Side
  * Plugin URI:        https://wordpress.org/plugins/gtm-server-side/
  * Description:       Enhance conversion tracking by implementing server-side tagging using server Google Tag Manager container. Effortlessly configure data layer events in web GTM, send webhooks, set up custom loader, and extend cookie lifetime.
- * Version:           2.1.23
+ * Version:           2.1.24
  * Author:            Stape
  * Author URI:        https://stape.io
  * License:           GPL-2.0+
@@ -32,6 +32,7 @@
 add_action( 'gtm_server_side', array( GTM_Server_Side_I18n::class, 'instance' ) );
 add_action( 'gtm_server_side', array( GTM_Server_Side_Webhook_Purchase::class, 'instance' ) );
 add_action( 'gtm_server_side', array( GTM_Server_Side_Webhook_Processing::class, 'instance' ) );
+add_action( 'gtm_server_side', array( GTM_Server_Side_Webhook_Completed::class, 'instance' ) );
 add_action( 'gtm_server_side', array( GTM_Server_Side_Webhook_Refund::class, 'instance' ) );
 add_action( 'gtm_server_side_admin', array( GTM_Server_Side_Admin_Settings::class, 'instance' ) );
 add_action( 'gtm_server_side_admin', array( GTM_Server_Side_Admin_Ajax::class, 'instance' ) );
diff --git a/includes/class-gtm-server-side-admin-ajax.php b/includes/class-gtm-server-side-admin-ajax.php
index 6e91f63..03492fe 100644
--- a/includes/class-gtm-server-side-admin-ajax.php
+++ b/includes/class-gtm-server-side-admin-ajax.php
@@ -54,13 +54,19 @@ public function gtm_server_side_webhook_test() {
 			);
 		}
 
-		$is_refund     = GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_REFUND );
 		$is_purchase   = GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_PURCHASE );
 		$is_processing = GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_PROCESSING );
-		if ( empty( $is_purchase ) && empty( $is_refund ) && empty( $is_processing ) ) {
+		$is_completed  = GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED );
+		$is_refund     = GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_REFUND );
+		if (
+			empty( $is_purchase ) &&
+			empty( $is_processing ) &&
+			empty( $is_completed ) &&
+			empty( $is_refund )
+		) {
 			wp_send_json_error(
 				array(
-					'message' => __( 'Purchase or order paid or refund webhook is required.', 'gtm-server-side' ),
+					'message' => __( 'Purchase or order paid processing or order paid completed or refund webhook is required.', 'gtm-server-side' ),
 				)
 			);
 		}
@@ -74,6 +80,10 @@ public function gtm_server_side_webhook_test() {
 			$answer[] = $this->send_webhook_processing();
 		}
 
+		if ( ! empty( $is_completed ) ) {
+			$answer[] = $this->send_webhook_completed();
+		}
+
 		if ( ! empty( $is_refund ) ) {
 			$answer[] = $this->send_webhook_refund();
 		}
@@ -102,28 +112,7 @@ public function gtm_server_side_webhook_test() {
 	private function send_webhook_purchase() {
 		$request = array(
 			'event'     => 'purchase',
-			'ecommerce' => array(
-				'transaction_id' => '358',
-				'affiliation'    => 'test',
-				'value'          => 18.00,
-				'tax'            => 0,
-				'shipping'       => 0,
-				'currency'       => 'USD',
-				'coupon'         => 'test_coupon',
-				'items'          => array(
-					array(
-						'item_name'      => 'Beanie',
-						'item_brand'     => 'Stape',
-						'item_id'        => '15',
-						'item_sku'       => 'woo-beanie',
-						'price'          => 18.00,
-						'item_category'  => 'Clothing',
-						'item_category2' => 'Accessories',
-						'quantity'       => 1,
-						'index'          => 1,
-					),
-				),
-			),
+			'ecommerce' => $this->get_ecommerce_data(),
 		);
 
 		$result = $this->send_request( $request );
@@ -139,47 +128,49 @@ private function send_webhook_purchase() {
 	}
 
 	/**
-	 * Send webhooks processing (order paid).
+	 * Send webhooks processing (order paid processing).
 	 *
 	 * @return string
 	 */
 	private function send_webhook_processing() {
 		$request = array(
 			'event'     => 'order_paid',
-			'ecommerce' => array(
-				'transaction_id' => '358',
-				'affiliation'    => 'test',
-				'value'          => 18.00,
-				'tax'            => 0,
-				'shipping'       => 0,
-				'currency'       => 'USD',
-				'coupon'         => 'test_coupon',
-				'items'          => array(
-					array(
-						'item_name'      => 'Beanie',
-						'item_brand'     => 'Stape',
-						'item_id'        => '15',
-						'item_sku'       => 'woo-beanie',
-						'price'          => 18.00,
-						'item_category'  => 'Clothing',
-						'item_category2' => 'Accessories',
-						'quantity'       => 1,
-						'index'          => 1,
-					),
-				),
-			),
+			'ecommerce' => $this->get_ecommerce_data(),
 		);
 
 		$result = $this->send_request( $request );
 		if ( is_wp_error( $result ) ) {
 			wp_send_json_error(
 				array(
-					'message' => __( 'Some problem with Purchase webhook.', 'gtm-server-side' ),
+					'message' => __( 'Some problem with order paid processing webhook.', 'gtm-server-side' ),
+				)
+			);
+		}
+
+		return __( 'Order paid processing webhook sent.', 'gtm-server-side' );
+	}
+
+	/**
+	 * Send webhooks completed (order paid completed).
+	 *
+	 * @return string
+	 */
+	private function send_webhook_completed() {
+		$request = array(
+			'event'     => 'order_completed',
+			'ecommerce' => $this->get_ecommerce_data(),
+		);
+
+		$result = $this->send_request( $request );
+		if ( is_wp_error( $result ) ) {
+			wp_send_json_error(
+				array(
+					'message' => __( 'Some problem with order paid completed webhook.', 'gtm-server-side' ),
 				)
 			);
 		}
 
-		return __( 'Order paid webhook sent.', 'gtm-server-side' );
+		return __( 'Order paid completed webhook sent.', 'gtm-server-side' );
 	}
 
 	/**
@@ -244,7 +235,7 @@ private function send_request( $body ) {
 	}
 
 	/**
-	 * Return user request test data
+	 * Return user request test data.
 	 *
 	 * @return array
 	 */
@@ -275,4 +266,34 @@ private function get_request_user_data() {
 			'new_customer'        => 'false',
 		);
 	}
+
+	/**
+	 * Return ecommerce test data.
+	 *
+	 * @return array
+	 */
+	private function get_ecommerce_data() {
+		return array(
+			'transaction_id' => '358',
+			'affiliation'    => 'test',
+			'value'          => 18.00,
+			'tax'            => 0,
+			'shipping'       => 0,
+			'currency'       => 'USD',
+			'coupon'         => 'test_coupon',
+			'items'          => array(
+				array(
+					'item_name'      => 'Beanie',
+					'item_brand'     => 'Stape',
+					'item_id'        => '15',
+					'item_sku'       => 'woo-beanie',
+					'price'          => 18.00,
+					'item_category'  => 'Clothing',
+					'item_category2' => 'Accessories',
+					'quantity'       => 1,
+					'index'          => 1,
+				),
+			),
+		);
+	}
 }
diff --git a/includes/class-gtm-server-side-admin-settings.php b/includes/class-gtm-server-side-admin-settings.php
index cd5c648..39e1448 100644
--- a/includes/class-gtm-server-side-admin-settings.php
+++ b/includes/class-gtm-server-side-admin-settings.php
@@ -441,7 +441,7 @@ function() {
 		);
 		add_settings_field(
 			GTM_SERVER_SIDE_FIELD_WEBHOOKS_PROCESSING,
-			__( 'Order paid webhook', 'gtm-server-side' ),
+			__( 'Order paid webhook - processing', 'gtm-server-side' ),
 			function() {
 				echo '<input
 					type="checkbox"
@@ -450,7 +450,31 @@ function() {
 					' . checked( GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_PROCESSING ), 'yes', false ) . '
 					value="yes">';
 					echo '<br>';
-					printf( __( 'Order paid event will be sent whenever an order is paid (has "Processing" status as per <a href="%s" target="_blank">Woocommerce documentation</a>).', 'gtm-server-side' ), 'https://woocommerce.com/document/managing-orders/order-statuses/' ); // phpcs:ignore
+					printf( __( 'order_paid event will be sent whenever an order is paid (has "Processing" status as per <a href="%s" target="_blank">Woocommerce documentation</a>).', 'gtm-server-side' ), 'https://woocommerce.com/document/managing-orders/order-statuses/' ); // phpcs:ignore
+			},
+			GTM_SERVER_SIDE_ADMIN_SLUG,
+			GTM_SERVER_SIDE_ADMIN_GROUP_WEBHOOKS
+		);
+
+		register_setting(
+			GTM_SERVER_SIDE_ADMIN_GROUP,
+			GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED,
+			array(
+				'sanitize_callback' => 'GTM_Server_Side_Helpers::sanitize_bool',
+			)
+		);
+		add_settings_field(
+			GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED,
+			__( 'Order paid webhook - completed', 'gtm-server-side' ),
+			function() {
+				echo '<input
+					type="checkbox"
+					id="' . esc_attr( GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED ) . '"
+					name="' . esc_attr( GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED ) . '"
+					' . checked( GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED ), 'yes', false ) . '
+					value="yes">';
+					echo '<br>';
+					printf( __( 'order_completed event will be sent whenever order status becomes completed (has "Completed" status as per <a href="%s" target="_blank">Woocommerce documentation</a>).', 'gtm-server-side' ), 'https://woocommerce.com/document/managing-orders/order-statuses/' ); // phpcs:ignore
 			},
 			GTM_SERVER_SIDE_ADMIN_SLUG,
 			GTM_SERVER_SIDE_ADMIN_GROUP_WEBHOOKS
diff --git a/includes/class-gtm-server-side-wc-helpers.php b/includes/class-gtm-server-side-wc-helpers.php
index 34a5b67..44ece2f 100644
--- a/includes/class-gtm-server-side-wc-helpers.php
+++ b/includes/class-gtm-server-side-wc-helpers.php
@@ -96,6 +96,10 @@ public function get_order_data_layer_items( $items ) {
 		foreach ( $items as $item_loop ) {
 			$product = $item_loop->get_product();
 
+			if ( ! ( $product instanceof WC_Product ) ) {
+				continue;
+			}
+
 			$array             = $this->get_data_layer_item( $product );
 			$array['quantity'] = intval( $item_loop->get_quantity() );
 			$array['index']    = $index++;
@@ -118,6 +122,10 @@ public function get_cart_data_layer_items( $cart ) {
 		foreach ( $cart as $product_loop ) {
 			$product = $product_loop['data'];
 
+			if ( ! ( $product instanceof WC_Product ) ) {
+				continue;
+			}
+
 			$array             = $this->get_data_layer_item( $product );
 			$array['quantity'] = intval( $product_loop['quantity'] );
 			$array['index']    = $index++;
diff --git a/includes/class-gtm-server-side-webhook-completed.php b/includes/class-gtm-server-side-webhook-completed.php
new file mode 100644
index 0000000..db9ddad
--- /dev/null
+++ b/includes/class-gtm-server-side-webhook-completed.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Webhook Completed.
+ *
+ * @package    GTM_Server_Side
+ * @subpackage GTM_Server_Side/includes
+ * @since      2.0.0
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Webhook Completed.
+ */
+class GTM_Server_Side_Webhook_Completed {
+	use GTM_Server_Side_Singleton;
+
+	/**
+	 * Init.
+	 *
+	 * @return void
+	 */
+	public function init() {
+		if ( ! function_exists( 'WC' ) ) {
+			return;
+		}
+
+		add_action( 'woocommerce_order_status_completed', array( $this, 'woocommerce_order_status_completed' ) );
+	}
+
+	/**
+	 * Order change status to completed.
+	 *
+	 * @param  int $order_id Order id.
+	 * @return void
+	 */
+	public function woocommerce_order_status_completed( $order_id ) {
+		if ( ! GTM_Server_Side_Helpers::is_enable_webhook() ) {
+			return;
+		}
+
+		if ( GTM_SERVER_SIDE_FIELD_VALUE_YES !== GTM_Server_Side_Helpers::get_option( GTM_SERVER_SIDE_FIELD_WEBHOOKS_COMPLETED ) ) {
+			return;
+		}
+
+		$order = wc_get_order( $order_id );
+		if ( ! ( $order instanceof WC_Order ) ) {
+			return;
+		}
+
+		$request = array(
+			'event'     => 'order_completed',
+			'ecommerce' => array(
+				'transaction_id' => esc_attr( $order->get_order_number() ),
+				'affiliation'    => '',
+				'value'          => GTM_Server_Side_WC_Helpers::instance()->formatted_price( $order->get_total() ),
+				'tax'            => GTM_Server_Side_WC_Helpers::instance()->formatted_price( $order->get_total_tax() ),
+				'shipping'       => GTM_Server_Side_WC_Helpers::instance()->formatted_price( $order->get_shipping_total() ),
+				'currency'       => esc_attr( $order->get_currency() ),
+				'coupon'         => esc_attr( join( ',', $order->get_coupon_codes() ) ),
+				'items'          => GTM_Server_Side_WC_Helpers::instance()->get_order_data_layer_items( $order->get_items() ),
+			),
+			'user_data' => GTM_Server_Side_WC_Helpers::instance()->get_order_user_data( $order ),
+		);
+
+		$request_cookies = GTM_Server_Side_Helpers::get_request_cookies();
+
+		if ( ! empty( $request_cookies ) ) {
+			$request['cookies'] = $request_cookies;
+
+			if ( isset( $request_cookies['_dcid'] ) ) {
+				$request['client_id'] = $request_cookies['_dcid'];
+			}
+		}
+
+		/**
+		 * Allows modification of processing order webhook payload.
+		 *
+		 * @param array  $request Webhook payload data.
+		 * @param object $order   WC_Order instance.
+		 */
+		$request = apply_filters( 'gtm_server_side_processing_webhook_payload', $request, $order );
+
+		GTM_Server_Side_Helpers::send_webhook_request( $request );
+	}
+}
diff --git a/includes/class-gtm-server-side-webhook-processing.php b/includes/class-gtm-server-side-webhook-processing.php
index 907d3f9..eed7c17 100644
--- a/includes/class-gtm-server-side-webhook-processing.php
+++ b/includes/class-gtm-server-side-webhook-processing.php
@@ -29,7 +29,7 @@ public function init() {
 	}
 
 	/**
-	 * Order change status to processing (Order paid).
+	 * Order change status to processing.
 	 *
 	 * @param  int $order_id Order id.
 	 * @return void
diff --git a/includes/class-gtm-server-side-webhook-refund.php b/includes/class-gtm-server-side-webhook-refund.php
index 8bd700e..d204f26 100644
--- a/includes/class-gtm-server-side-webhook-refund.php
+++ b/includes/class-gtm-server-side-webhook-refund.php
@@ -52,7 +52,7 @@ public function woocommerce_order_refunded( $order_id, $refund_id ) {
 		$request = array(
 			'event'     => 'refund',
 			'ecommerce' => array(
-				'transaction_id' => $refund_id,
+				'transaction_id' => esc_attr( $order->get_order_number() ),
 				'value'          => GTM_Server_Side_WC_Helpers::instance()->formatted_price( $order->get_total() ),
 				'currency'       => esc_attr( $order->get_currency() ),
 				'items'          => GTM_Server_Side_WC_Helpers::instance()->get_order_data_layer_items( $order->get_items() ),

From 09b93d0fc5b344c776c6aca98b9ec80281d25c95 Mon Sep 17 00:00:00 2001
From: Pavel <pavel.lance@gmail.com>
Date: Tue, 26 Nov 2024 11:28:30 +0200
Subject: [PATCH 2/2] Added cart_hash key to webhooks

---
 README.txt                                            | 1 +
 includes/class-gtm-server-side-webhook-completed.php  | 1 +
 includes/class-gtm-server-side-webhook-processing.php | 1 +
 includes/class-gtm-server-side-webhook-purchase.php   | 1 +
 includes/class-gtm-server-side-webhook-refund.php     | 1 +
 5 files changed, 5 insertions(+)

diff --git a/README.txt b/README.txt
index 69e4718..bdac595 100644
--- a/README.txt
+++ b/README.txt
@@ -69,6 +69,7 @@ Yes. <a href="https://stape.io/blog/how-to-set-up-facebook-conversion-api">How t
 
 = 2.1.24 =
 * Added Order paid completed webhook
+* Added cart_hash key to webhooks
 
 = 2.1.23 =
 * Fix purchase event
diff --git a/includes/class-gtm-server-side-webhook-completed.php b/includes/class-gtm-server-side-webhook-completed.php
index db9ddad..080444e 100644
--- a/includes/class-gtm-server-side-webhook-completed.php
+++ b/includes/class-gtm-server-side-webhook-completed.php
@@ -50,6 +50,7 @@ public function woocommerce_order_status_completed( $order_id ) {
 
 		$request = array(
 			'event'     => 'order_completed',
+			'cart_hash' => $order->get_cart_hash(),
 			'ecommerce' => array(
 				'transaction_id' => esc_attr( $order->get_order_number() ),
 				'affiliation'    => '',
diff --git a/includes/class-gtm-server-side-webhook-processing.php b/includes/class-gtm-server-side-webhook-processing.php
index eed7c17..a64f4b4 100644
--- a/includes/class-gtm-server-side-webhook-processing.php
+++ b/includes/class-gtm-server-side-webhook-processing.php
@@ -50,6 +50,7 @@ public function woocommerce_order_status_processing( $order_id ) {
 
 		$request = array(
 			'event'     => 'order_paid',
+			'cart_hash' => $order->get_cart_hash(),
 			'ecommerce' => array(
 				'transaction_id' => esc_attr( $order->get_order_number() ),
 				'affiliation'    => '',
diff --git a/includes/class-gtm-server-side-webhook-purchase.php b/includes/class-gtm-server-side-webhook-purchase.php
index 2b001d6..23d3af3 100644
--- a/includes/class-gtm-server-side-webhook-purchase.php
+++ b/includes/class-gtm-server-side-webhook-purchase.php
@@ -50,6 +50,7 @@ public function woocommerce_new_order( $order_id, $order ) {
 
 		$request                              = array(
 			'event'     => 'purchase',
+			'cart_hash' => $order->get_cart_hash(),
 			'ecommerce' => array(
 				'transaction_id' => esc_attr( $order->get_order_number() ),
 				'affiliation'    => '',
diff --git a/includes/class-gtm-server-side-webhook-refund.php b/includes/class-gtm-server-side-webhook-refund.php
index d204f26..7af9915 100644
--- a/includes/class-gtm-server-side-webhook-refund.php
+++ b/includes/class-gtm-server-side-webhook-refund.php
@@ -51,6 +51,7 @@ public function woocommerce_order_refunded( $order_id, $refund_id ) {
 
 		$request = array(
 			'event'     => 'refund',
+			'cart_hash' => $order->get_cart_hash(),
 			'ecommerce' => array(
 				'transaction_id' => esc_attr( $order->get_order_number() ),
 				'value'          => GTM_Server_Side_WC_Helpers::instance()->formatted_price( $order->get_total() ),