diff --git a/includes/Core/Ecommerce/Platforms/AbstractPlatform.php b/includes/Core/Ecommerce/Platforms/AbstractPlatform.php index db3c81d..06cffe4 100644 --- a/includes/Core/Ecommerce/Platforms/AbstractPlatform.php +++ b/includes/Core/Ecommerce/Platforms/AbstractPlatform.php @@ -13,4 +13,8 @@ abstract class AbstractPlatform implements PlatformInterface { public function is_integrated() { return Settings::instance()->is_integrated() && Settings::instance()->platform() === $this->get_name(); } + + public function is_subscriptions_active() { + return false; + } } diff --git a/includes/Core/Ecommerce/Platforms/PlatformInterface.php b/includes/Core/Ecommerce/Platforms/PlatformInterface.php index 2ed79b4..6100b2a 100644 --- a/includes/Core/Ecommerce/Platforms/PlatformInterface.php +++ b/includes/Core/Ecommerce/Platforms/PlatformInterface.php @@ -18,4 +18,6 @@ public function categories( array $args = array() ); public function register_hooks(); public function get_name(); + + public function is_subscriptions_active(); } diff --git a/includes/Core/Ecommerce/Platforms/WooCommerce.php b/includes/Core/Ecommerce/Platforms/WooCommerce.php index 3c1294c..e34c63b 100644 --- a/includes/Core/Ecommerce/Platforms/WooCommerce.php +++ b/includes/Core/Ecommerce/Platforms/WooCommerce.php @@ -8,6 +8,7 @@ use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\CategoryResource; use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\OrderResource; use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\ProductResource; +use WeDevs\WeMail\Rest\Resources\Ecommerce\WooCommerce\SubscriptionResource; use WeDevs\WeMail\Traits\Singleton; use WP_Post; @@ -68,6 +69,43 @@ public function products( array $args = array() ) { ); } + /** + * Get subscriptions from WooCommerce store + * + * @param array $args + * + * @return array + */ + public function subscriptions( array $args = array() ) { + if ( ! $this->is_subscriptions_active() ) { + return array( + 'data' => array(), + 'total' => 0, + 'current_page' => 1, + 'total_page' => 0, + ); + } + + $args = wp_parse_args( + $args, + array( + 'limit' => isset( $args['limit'] ) ? intval( $args['limit'] ) : 100, + 'page' => isset( $args['page'] ) ? intval( $args['page'] ) : 1, + 'paginate' => true, + 'type' => 'shop_subscription', + ) + ); + + $data = wc_get_orders( $args ); + + return array( + 'data' => SubscriptionResource::collection( $data->orders ), + 'total' => $data->total, + 'current_page' => intval( $args['page'] ), + 'total_page' => $data->max_num_pages, + ); + } + /** * Get orders from WooCommerce store * @@ -113,7 +151,6 @@ public function register_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 ); @@ -121,9 +158,6 @@ public function register_hooks() { // Order status changed hook (handles pending payment, completed, etc.) add_action( 'woocommerce_order_status_changed', array( $this, 'handle_order_status_changed' ), 10, 4 ); - // Pending payment status specific hook - add_action( 'woocommerce_order_status_pending', array( $this, 'handle_pending_payment' ), 10, 1 ); - add_action( 'woocommerce_order_refunded', array( $this, 'create_order_refund' ), 10, 2 ); add_action( 'woocommerce_refund_deleted', array( $this, 'delete_order_refund' ), 10, 2 ); add_action( 'after_delete_post', array( $this, 'delete' ), 10, 2 ); @@ -135,6 +169,12 @@ public function register_hooks() { add_action( 'created_product_cat', array( $this, 'handle_category' ), 10, 2 ); add_action( 'edited_product_cat', array( $this, 'handle_category' ), 10, 2 ); add_action( 'delete_product_cat', array( $this, 'handle_category_delete' ), 10, 3 ); + + // WooCommerce Subscriptions hooks + add_action( 'woocommerce_new_subscription', array( $this, 'handle_new_subscription' ), 10, 1 ); + add_action( 'woocommerce_subscription_status_cancelled', array( $this, 'handle_subscription_cancelled' ), 10, 1 ); + add_action( 'woocommerce_subscription_status_expired', array( $this, 'handle_subscription_expired' ), 10, 1 ); + add_action( 'woocommerce_subscription_renewal_payment_complete', array( $this, 'handle_subscription_renewal_payment_complete' ), 10, 1 ); } /** @@ -180,19 +220,6 @@ public function handle_remove_cart_item( $cart_item_key, $cart ) { $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 */ @@ -318,37 +345,6 @@ private function send_cart_data( $event ) { ->put( $payload ); } - /** - * Handle pending payment status - * - * @param int $order_id Order ID - */ - public function handle_pending_payment( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - return; - } - - if ( ! $this->is_valid_order_item( $order->get_type() ) ) { - return; - } - - if ( ! Settings::instance()->is_enabled() ) { - return; - } - - $payload = OrderResource::single( $order ); - - wemail()->api - ->send_json() - ->ecommerce() - ->orders( $order_id ) - ->put( $payload ); - - $this->reset_cart_key(); - } - /** * Handle new order created * @@ -570,6 +566,15 @@ public function is_active() { return class_exists( 'WooCommerce' ); } + /** + * Check if WooCommerce Subscriptions is active + * + * @return bool + */ + public function is_subscriptions_active() { + return class_exists( 'WC_Subscription' ); + } + /** * Get integration name * @@ -672,4 +677,78 @@ public function handle_category_delete( $term_id, $tt_id, $deleted_term ) { ->categories( $term_id ) ->delete(); } + + /** + * Handle new subscription created + * + * @param \WC_Subscription $subscription Subscription object + */ + public function handle_new_subscription( $subscription ) { + if ( is_numeric( $subscription ) ) { + $subscription = wcs_get_subscription( $subscription ); + } + + if ( ! $subscription ) { + return; + } + + $this->send_subscription_data( $subscription, 'subscription_created' ); + } + + /** + * Handle subscription cancelled + * + * @param \WC_Subscription $subscription Subscription object + */ + public function handle_subscription_cancelled( $subscription ) { + $this->send_subscription_data( $subscription, 'subscription_cancelled' ); + } + + /** + * Handle subscription expired + * + * @param \WC_Subscription $subscription Subscription object + */ + public function handle_subscription_expired( $subscription ) { + $this->send_subscription_data( $subscription, 'subscription_expired' ); + } + + /** + * Handle subscription renewal payment complete + * + * @param \WC_Subscription $subscription Subscription object + */ + public function handle_subscription_renewal_payment_complete( $subscription ) { + $this->send_subscription_data( + $subscription, 'subscription_renewal_payment_complete', array( + 'renewal_complete' => true, + ) + ); + } + /** + * Send subscription data to weMail API + * + * @param \WC_Subscription $subscription Subscription object + * @param string $event Event type + * @param array $extra Additional payload data + */ + private function send_subscription_data( $subscription, $event, $extra = array() ) { + if ( ! $subscription ) { + return; + } + + if ( ! Settings::instance()->is_enabled() ) { + return; + } + + $payload = SubscriptionResource::single( $subscription ); + $payload['event'] = $event; + $payload = array_merge( $payload, $extra ); + + wemail()->api + ->send_json() + ->ecommerce() + ->subscriptions( $subscription->get_id() ) + ->put( $payload ); + } } diff --git a/includes/Rest/Ecommerce/Ecommerce.php b/includes/Rest/Ecommerce/Ecommerce.php index 0ec8aff..df02cd7 100644 --- a/includes/Rest/Ecommerce/Ecommerce.php +++ b/includes/Rest/Ecommerce/Ecommerce.php @@ -20,6 +20,8 @@ class Ecommerce extends RestController { public function register_routes() { $this->post( '/(?Pwoocommerce|edd)/settings', 'settings' ); $this->get( '/(?Pwoocommerce|edd)/(?Pproducts|orders|categories)', 'resource', 'permission' ); + $this->get( '/(?Pwoocommerce)/(?Psubscriptions)', 'resource', 'permission' ); + $this->get( '/(?Pwoocommerce)/status', 'status', 'permission' ); $this->post( '/coupons', 'create_coupon', 'permission' ); $this->delete( '/disconnect', 'disconnect', 'permission' ); } @@ -38,9 +40,10 @@ public function settings( $request ) { $body = array_merge( $body, array( - 'currency' => $platform->currency(), - 'currency_symbol' => $platform->currency_symbol(), - 'platform' => $platform->get_name(), + 'currency' => $platform->currency(), + 'currency_symbol' => $platform->currency_symbol(), + 'platform' => $platform->get_name(), + 'subscriptions_enabled' => $platform->is_subscriptions_active(), ) ); // Update settings to wp options @@ -59,6 +62,25 @@ public function settings( $request ) { return new \WP_REST_Response( $response ); } + /** + * Platform status endpoint + * + * @param \WP_REST_Request $request + */ + public function status( $request ) { + $platform = $request->get_param( 'source' ); + + /** @var PlatformInterface $platform */ + $platform = wemail()->ecommerce->platform( $platform ); + + return new \WP_REST_Response( + array( + 'is_active' => $platform->is_active(), + 'subscriptions_enabled' => $platform->is_subscriptions_active(), + ) + ); + } + /** * Access products, orders or customers resource * diff --git a/includes/Rest/Resources/Ecommerce/WooCommerce/ProductResource.php b/includes/Rest/Resources/Ecommerce/WooCommerce/ProductResource.php index d9bc783..1e13ca9 100644 --- a/includes/Rest/Resources/Ecommerce/WooCommerce/ProductResource.php +++ b/includes/Rest/Resources/Ecommerce/WooCommerce/ProductResource.php @@ -23,6 +23,7 @@ public function blueprint( $resource ) { 'permalink' => $resource->get_permalink(), 'thumbnail' => $thumbnail ? $thumbnail : null, 'categories' => $resource->get_category_ids(), + 'type' => $resource->get_type(), 'source' => 'woocommerce', ); } diff --git a/includes/Rest/Resources/Ecommerce/WooCommerce/SubscriptionResource.php b/includes/Rest/Resources/Ecommerce/WooCommerce/SubscriptionResource.php new file mode 100644 index 0000000..1647441 --- /dev/null +++ b/includes/Rest/Resources/Ecommerce/WooCommerce/SubscriptionResource.php @@ -0,0 +1,177 @@ +get_billing_source( $subscription ); + $items = array_filter( + $source->get_items(), function ( $item ) { + return $item->get_product_id(); + } + ); + + return array( + 'id' => (string) $subscription->get_id(), + 'status' => $subscription->get_status(), + 'customer_id' => $this->get_customer_id( $this->get_billing_email( $subscription ) ), + 'customer' => $this->customer( $subscription ), + 'total' => floatval( $subscription->get_total() ), + 'product_ids' => $this->get_product_ids( $items ), + 'categories' => $this->get_category_ids( $items ), + 'billing_period' => $subscription->get_billing_period(), + 'billing_interval' => intval( $subscription->get_billing_interval() ), + 'trial_end_date' => $this->format_date( $subscription->get_date( 'trial_end' ) ), + 'next_payment_date' => $this->format_date( $subscription->get_date( 'next_payment' ) ), + 'end_date' => $this->format_date( $subscription->get_date( 'end' ) ), + 'permalink' => $subscription->get_view_order_url(), + 'created_at' => $subscription->get_date_created() ? $subscription->get_date_created()->setTimezone( new DateTimeZone( 'UTC' ) )->format( self::DATE_FORMAT ) : null, + 'updated_at' => $subscription->get_date_modified() ? $subscription->get_date_modified()->setTimezone( new DateTimeZone( 'UTC' ) )->format( self::DATE_FORMAT ) : null, + ); + } + + /** + * Return the authoritative billing source (parent order when available). + * Avoids stale session data that may be on the subscription object when + * woocommerce_new_subscription fires before billing details are persisted. + * + * @param \WC_Subscription $subscription + * + * @return \WC_Order|\WC_Subscription + */ + protected function get_billing_source( $subscription ) { + $parent_order = $subscription->get_parent(); + + if ( $parent_order && $parent_order->get_billing_email() ) { + return $parent_order; + } + + return $subscription; + } + + /** + * Get billing email from the authoritative billing source. + * + * @param \WC_Subscription $subscription + * + * @return string + */ + protected function get_billing_email( $subscription ) { + return $this->get_billing_source( $subscription )->get_billing_email(); + } + + /** + * Transform customer data + * + * @param \WC_Subscription $subscription + * + * @return array + */ + protected function customer( $subscription ) { + $source = $this->get_billing_source( $subscription ); + + return array( + 'email' => $source->get_billing_email(), + 'first_name' => $source->get_billing_first_name(), + 'last_name' => $source->get_billing_last_name(), + 'address_1' => $source->get_billing_address_1(), + 'address_2' => $source->get_billing_address_2(), + 'city' => $source->get_billing_city(), + 'state' => $source->get_billing_state(), + 'postcode' => $source->get_billing_postcode(), + 'country' => $source->get_billing_country(), + 'phone' => $source->get_billing_phone(), + ); + } + + /** + * Get customer ID + * + * @param string $email + * + * @return string + */ + protected function get_customer_id( $email ) { + return md5( trim( strtolower( $email ) ) ); + } + + /** + * Get product IDs from subscription items + * + * @param array $items + * + * @return array + */ + protected function get_product_ids( $items ) { + $product_ids = array(); + + foreach ( $items as $item ) { + $product_ids[] = (string) $item->get_product_id(); + } + + return array_unique( $product_ids ); + } + + /** + * Get category IDs from subscription items + * + * @param array $items + * + * @return array + */ + protected function get_category_ids( $items ) { + $category_ids = array(); + + foreach ( $items as $item ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + $cats = $product->get_category_ids(); + + foreach ( $cats as $cat_id ) { + $category_ids[] = (string) $cat_id; + } + } + + return array_values( array_unique( $category_ids ) ); + } + + /** + * Format a subscription date string + * + * @param string $date_string + * + * @return string|null + */ + protected function format_date( $date_string ) { + if ( empty( $date_string ) || $date_string === '0' ) { + return null; + } + + try { + $date = new \DateTime( $date_string, new DateTimeZone( 'UTC' ) ); + return $date->format( self::DATE_FORMAT ); + } catch ( \Exception $e ) { + return null; + } + } +}