Skip to content
10 changes: 10 additions & 0 deletions includes/Core/AbandonedCarts/AbandonedCarts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace WeDevs\WeMail\Core\AbandonedCarts;

use WeDevs\WeMail\Traits\Core;

class AbandonedCarts {

use Core;
}
48 changes: 48 additions & 0 deletions includes/Core/AbandonedCarts/Menu.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace WeDevs\WeMail\Core\AbandonedCarts;

use WeDevs\WeMail\Traits\Hooker;

class Menu {

use Hooker;

/**
* Submenu priority
*
* @since 1.0.0
*
* @var integer
*/
public $menu_priority = 86;

/**
* Class constructor
*
* @since 1.0.0
*
* @return void
*/
public function __construct() {
$this->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;
}
}
202 changes: 202 additions & 0 deletions includes/Core/Ecommerce/Platforms/WooCommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*
Expand Down Expand Up @@ -98,6 +106,15 @@
* 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 );

Expand All @@ -120,6 +137,187 @@
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'] ) ) {

Check warning on line 200 in includes/Core/Ecommerce/Platforms/WooCommerce.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Processing form data without nonce verification.
return;
}

$token = sanitize_text_field( wp_unslash( $_GET['wemail-recover-cart'] ) );

Check warning on line 204 in includes/Core/Ecommerce/Platforms/WooCommerce.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Processing form data without nonce verification.

if ( empty( $token ) ) {
return;
}

if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
return;
}

$coupon_code = isset( $_GET['coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['coupon'] ) ) : '';

Check warning on line 214 in includes/Core/Ecommerce/Platforms/WooCommerce.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Processing form data without nonce verification.

Check warning on line 214 in includes/Core/Ecommerce/Platforms/WooCommerce.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Processing form data without nonce verification.

$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
*
Expand Down Expand Up @@ -147,6 +345,8 @@
->ecommerce()
->orders( $order_id )
->put( $payload );

$this->reset_cart_key();
}

/**
Expand Down Expand Up @@ -203,6 +403,8 @@
->ecommerce()
->orders( $order_id )
->put( $payload );

$this->reset_cart_key();
}

/**
Expand Down
21 changes: 21 additions & 0 deletions includes/Core/Shortcode/Shortcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ];
}
Expand Down
53 changes: 53 additions & 0 deletions includes/Rest/Ecommerce/Ecommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Ecommerce extends RestController {
public function register_routes() {
$this->post( '/(?P<source>woocommerce|edd)/settings', 'settings' );
$this->get( '/(?P<source>woocommerce|edd)/(?P<resource>products|orders|categories)', 'resource', 'permission' );
$this->post( '/coupons', 'create_coupon', 'permission' );
$this->delete( '/disconnect', 'disconnect', 'permission' );
}

Expand Down Expand Up @@ -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
*
Expand Down
Loading
Loading