diff --git a/includes/Core/AbandonedCarts/AbandonedCarts.php b/includes/Core/AbandonedCarts/AbandonedCarts.php new file mode 100644 index 0000000..61ed2db --- /dev/null +++ b/includes/Core/AbandonedCarts/AbandonedCarts.php @@ -0,0 +1,10 @@ +add_filter( 'wemail_admin_submenu', 'register_submenu', $this->menu_priority, 2 ); + } + + /** + * Register submenu + * + * @since 1.0.0 + * + * @param array $menu_items + * @param string $capability + * + * @return array + */ + public function register_submenu( $menu_items, $capability ) { + if ( wemail()->user->can( 'view_wemail' ) ) { + $menu_items[] = array( __( 'Abandoned Carts', 'wemail' ), $capability, 'admin.php?page=wemail#/abandoned-carts' ); + } + + return $menu_items; + } +} diff --git a/includes/Core/Ecommerce/Platforms/WooCommerce.php b/includes/Core/Ecommerce/Platforms/WooCommerce.php index 2ba8822..3c1294c 100644 --- a/includes/Core/Ecommerce/Platforms/WooCommerce.php +++ b/includes/Core/Ecommerce/Platforms/WooCommerce.php @@ -4,6 +4,7 @@ use WeDevs\WeMail\Core\Ecommerce\Settings; use WeDevs\WeMail\Core\Sync\Ecommerce\RevenueTrack; +use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\CartResource; use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\CategoryResource; use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\OrderResource; use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\ProductResource; @@ -13,6 +14,13 @@ class WooCommerce extends AbstractPlatform { use Singleton; + /** + * Flag to prevent recursive hook firing during cart recovery + * + * @var bool + */ + private $is_recovering = false; + /** * Get currency * @@ -98,6 +106,15 @@ public function orders( array $args = array() ) { * Register post update hooks */ public function register_hooks() { + // Cart recovery hook + add_action( 'template_redirect', array( $this, 'handle_cart_recovery' ) ); + + // Cart hooks + add_action( 'woocommerce_add_to_cart', array( $this, 'handle_add_to_cart' ), 10, 6 ); + add_action( 'woocommerce_cart_updated', array( $this, 'handle_cart_updated' ), 10, 0 ); + add_action( 'woocommerce_remove_cart_item', array( $this, 'handle_remove_cart_item' ), 10, 2 ); + add_action( 'woocommerce_cart_emptied', array( $this, 'handle_cart_emptied' ), 10, 0 ); + // New order created hook add_action( 'woocommerce_new_order', array( $this, 'handle_new_order' ), 10, 2 ); @@ -120,6 +137,187 @@ public function register_hooks() { add_action( 'delete_product_cat', array( $this, 'handle_category_delete' ), 10, 3 ); } + /** + * Handle add to cart event + * + * @param string $cart_item_key Cart item key + * @param int $product_id Product ID + * @param int $quantity Quantity + * @param int $variation_id Variation ID + * @param array $variation Variation data + * @param array $cart_item_data Cart item data + */ + public function handle_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) { + if ( $this->is_recovering || ! Settings::instance()->is_enabled() ) { + return; + } + + $this->send_cart_data( 'add_to_cart' ); + } + + /** + * Handle cart updated event + */ + public function handle_cart_updated() { + if ( $this->is_recovering || ! Settings::instance()->is_enabled() ) { + return; + } + + $this->send_cart_data( 'cart_updated' ); + } + + /** + * Handle remove cart item event + * + * @param string $cart_item_key Cart item key + * @param \WC_Cart $cart Cart object + */ + public function handle_remove_cart_item( $cart_item_key, $cart ) { + if ( $this->is_recovering || ! Settings::instance()->is_enabled() ) { + return; + } + + $this->send_cart_data( 'remove_cart_item' ); + } + + /** + * Handle cart emptied event + */ + public function handle_cart_emptied() { + if ( $this->is_recovering || ! Settings::instance()->is_enabled() ) { + return; + } + + $this->send_cart_data( 'cart_emptied' ); + + $this->reset_cart_key(); + } + + /** + * Handle cart recovery from abandoned cart email link + */ + public function handle_cart_recovery() { + if ( ! isset( $_GET['wemail-recover-cart'] ) ) { + return; + } + + $token = sanitize_text_field( wp_unslash( $_GET['wemail-recover-cart'] ) ); + + if ( empty( $token ) ) { + return; + } + + if ( ! function_exists( 'WC' ) || ! WC()->cart ) { + return; + } + + $coupon_code = isset( $_GET['coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['coupon'] ) ) : ''; + + $response = wemail()->api + ->ecommerce() + ->abandoned_carts() + ->recover() + ->query( array( 'token' => $token ) ) + ->get(); + + if ( is_wp_error( $response ) || empty( $response['data']['items'] ) ) { + wp_safe_redirect( wc_get_checkout_url() ); + exit; + } + + $this->is_recovering = true; + + WC()->cart->empty_cart(); + + $items = $response['data']['items']; + + foreach ( $items as $item ) { + $product_id = isset( $item['product_id'] ) ? absint( $item['product_id'] ) : 0; + $quantity = isset( $item['quantity'] ) ? absint( $item['quantity'] ) : 1; + $variation_id = isset( $item['variation_id'] ) ? absint( $item['variation_id'] ) : 0; + $variation_attr = isset( $item['variation'] ) ? (array) $item['variation'] : array(); + + if ( $product_id ) { + try { + WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation_attr ); + } catch ( \Exception $e ) { + error_log( 'weMail cart recovery: failed to add product ' . $product_id . ' - ' . $e->getMessage() ); + } + } + } + + if ( ! empty( $coupon_code ) && wc_get_coupon_id_by_code( $coupon_code ) ) { + WC()->cart->apply_coupon( $coupon_code ); + } + + WC()->cart->calculate_totals(); + + $this->is_recovering = false; + + wp_safe_redirect( wc_get_checkout_url() ); + exit; + } + + /** + * Get or generate cart key + * + * @return string Cart key + */ + private function get_cart_key() { + $cart_key = WC()->session->get( 'wem_cart_key' ); + + if ( ! $cart_key ) { + $cart_key = wp_generate_uuid4(); + WC()->session->set( 'wem_cart_key', $cart_key ); + } + + return $cart_key; + } + + /** + * Reset cart key for current session. + * + * @return void + */ + private function reset_cart_key() { + if ( function_exists( 'WC' ) && WC()->session ) { + WC()->session->__unset( 'wem_cart_key' ); + } + } + + /** + * Send cart data to weMail API + * + * @param string $event Event type + */ + private function send_cart_data( $event ) { + if ( ! WC()->session || ! WC()->session->get_customer_id() ) { + return; + } + + $cart = WC()->cart; + + if ( ! $cart ) { + return; + } + + // Don't send if cart is empty (except for cart_emptied event) + if ( $event !== 'cart_emptied' && $cart->is_empty() ) { + return; + } + + $cart_key = $this->get_cart_key(); + $payload = CartResource::with_customer( $cart, $cart_key ); + $payload['event'] = $event; + $payload['session_id'] = WC()->session->get_customer_id(); + + wemail()->api + ->send_json() + ->ecommerce() + ->carts() + ->put( $payload ); + } + /** * Handle pending payment status * @@ -147,6 +345,8 @@ public function handle_pending_payment( $order_id ) { ->ecommerce() ->orders( $order_id ) ->put( $payload ); + + $this->reset_cart_key(); } /** @@ -203,6 +403,8 @@ private function process_order( $order_id, $order ) { ->ecommerce() ->orders( $order_id ) ->put( $payload ); + + $this->reset_cart_key(); } /** diff --git a/includes/Core/Shortcode/Shortcode.php b/includes/Core/Shortcode/Shortcode.php index b79dc51..3ba83c2 100644 --- a/includes/Core/Shortcode/Shortcode.php +++ b/includes/Core/Shortcode/Shortcode.php @@ -110,6 +110,27 @@ public function get( $type = '' ) { ), ); + $shortcodes['abandoned_cart'] = array( + 'title' => __( 'Abandoned Cart', 'wemail' ), + 'codes' => array( + 'items' => array( + 'title' => __( 'Cart Items Table', 'wemail' ), + ), + 'total' => array( + 'title' => __( 'Cart Total', 'wemail' ), + ), + 'recover_url' => array( + 'title' => __( 'Cart Recovery Link', 'wemail' ), + ), + 'count' => array( + 'title' => __( 'Number of Items', 'wemail' ), + ), + 'coupon_code' => array( + 'title' => __( 'Coupon Code', 'wemail' ), + ), + ), + ); + if ( ! empty( $type ) && ! empty( $shortcodes[ $type ] ) ) { return $shortcodes[ $type ]; } diff --git a/includes/Rest/Ecommerce/Ecommerce.php b/includes/Rest/Ecommerce/Ecommerce.php index 43d59d2..0ec8aff 100644 --- a/includes/Rest/Ecommerce/Ecommerce.php +++ b/includes/Rest/Ecommerce/Ecommerce.php @@ -20,6 +20,7 @@ class Ecommerce extends RestController { public function register_routes() { $this->post( '/(?Pwoocommerce|edd)/settings', 'settings' ); $this->get( '/(?Pwoocommerce|edd)/(?Pproducts|orders|categories)', 'resource', 'permission' ); + $this->post( '/coupons', 'create_coupon', 'permission' ); $this->delete( '/disconnect', 'disconnect', 'permission' ); } @@ -107,6 +108,58 @@ public function disconnect() { return new \WP_REST_Response( $response ); } + /** + * Create a WooCommerce coupon + * + * @param \WP_REST_Request $request + * + * @return \WP_REST_Response|\WP_Error + */ + public function create_coupon( $request ) { + if ( ! function_exists( 'WC' ) ) { + return $this->respond_error( 'WooCommerce is not active.', 'woocommerce_inactive', self::HTTP_UNPROCESSABLE_ENTITY ); + } + + $data = $request->get_json_params(); + + if ( empty( $data['code'] ) ) { + return $this->respond_error( 'Coupon code is required.', 'missing_coupon_code', self::HTTP_UNPROCESSABLE_ENTITY ); + } + + $existing = wc_get_coupon_id_by_code( $data['code'] ); + + if ( $existing ) { + return $this->respond_error( 'Coupon code already exists.', 'coupon_exists', self::HTTP_CONFLICT ); + } + + $coupon = new \WC_Coupon(); + + $coupon->set_code( sanitize_text_field( $data['code'] ) ); + $coupon->set_discount_type( isset( $data['discount_type'] ) ? sanitize_text_field( $data['discount_type'] ) : 'percent' ); + $coupon->set_amount( isset( $data['amount'] ) ? floatval( $data['amount'] ) : 0 ); + $coupon->set_usage_limit( isset( $data['usage_limit'] ) ? absint( $data['usage_limit'] ) : 1 ); + $coupon->set_individual_use( ! empty( $data['individual_use'] ) ); + $coupon->set_free_shipping( ! empty( $data['free_shipping'] ) ); + + if ( ! empty( $data['date_expires'] ) ) { + $coupon->set_date_expires( $data['date_expires'] ); + } + + if ( ! empty( $data['email_restrictions'] ) && is_array( $data['email_restrictions'] ) ) { + $coupon->set_email_restrictions( array_map( 'sanitize_email', $data['email_restrictions'] ) ); + } + + $coupon->save(); + + return $this->respond( + array( + 'id' => $coupon->get_id(), + 'code' => $coupon->get_code(), + ), + self::HTTP_CREATED + ); + } + /** * Rest permission * diff --git a/includes/Rest/Resources/Ecommerce/WooCommerce/CartResource.php b/includes/Rest/Resources/Ecommerce/WooCommerce/CartResource.php new file mode 100644 index 0000000..45ea90d --- /dev/null +++ b/includes/Rest/Resources/Ecommerce/WooCommerce/CartResource.php @@ -0,0 +1,92 @@ +get_cart(); + $items = array(); + + foreach ( $cart_items as $cart_item_key => $cart_item ) { + $product = $cart_item['data']; + $permalink = $product->get_permalink(); + $thumbnail = wp_get_attachment_image_url( $product->get_image_id() ); + $product_id = isset( $cart_item['product_id'] ) ? $cart_item['product_id'] : $product->get_id(); + $variation_id = isset( $cart_item['variation_id'] ) ? $cart_item['variation_id'] : 0; + + $items[] = array( + 'cart_item_key' => $cart_item_key, + 'product_id' => (string) $product_id, + 'variation_id' => (string) $variation_id, + 'name' => $product->get_name(), + 'quantity' => $cart_item['quantity'], + 'total' => floatval( $cart_item['line_total'] ), + 'thumbnail' => $thumbnail ? $thumbnail : null, + 'categories' => $product->get_category_ids(), + 'permalink' => $permalink ? $permalink : null, + ); + } + + return array( + 'items' => $items, + 'total' => floatval( $cart->get_total( 'raw' ) ), + 'subtotal' => floatval( $cart->get_subtotal() ), + 'currency' => get_woocommerce_currency(), + 'updated_at' => current_time( self::DATE_FORMAT, true ), + 'checkout_url' => wc_get_checkout_url(), + 'home_url' => home_url(), + 'source' => 'woocommerce', + ); + } + + /** + * Get cart with customer data and cart key + * + * @param \WC_Cart $cart + * @param string $cart_key + * @return array + */ + public static function with_customer( $cart, $cart_key = null ) { + $data = self::single( $cart ); + $current_user = wp_get_current_user(); + + // Add cart key + $data['cart_key'] = $cart_key; + + if ( $current_user->exists() ) { + $data['customer'] = array( + 'id' => (string) $current_user->ID, + 'first_name' => $current_user->first_name, + 'last_name' => $current_user->last_name, + 'email' => $current_user->user_email, + ); + } else { + // Try to get guest checkout data from WooCommerce session + $customer = WC()->session->get( 'customer' ); + if ( $customer ) { + $data['customer'] = array( + 'first_name' => isset( $customer['first_name'] ) ? $customer['first_name'] : null, + 'last_name' => isset( $customer['last_name'] ) ? $customer['last_name'] : null, + 'email' => isset( $customer['email'] ) ? $customer['email'] : null, + ); + } + } + + return $data; + } +}