0 ) { $trial_interval = isset( $row['plan_data']['free_trial_interval'] ) ? (string) $row['plan_data']['free_trial_interval'] : 'days'; $trial = (int) $row['free_trial'] . ' ' . $trial_interval; } return array( 'price' => (float) PlanPresenter::offer_price( $regular, $selling, $dtype, $dvalue ), 'time' => max( 1, (int) $row['billing_frequency'] ), 'option' => PlanRepository::interval_to_option( (int) $row['billing_interval'] ), 'trial' => $trial, ); } /** * Stamp the chosen plan onto the cart item. * * Reads `subscrpt_plan_id` from the add-to-cart request, resolves the term, * and writes the plan id + a `subscription` array (same shape the classic * engine uses) so cart display and calculation work unchanged. * * @param array $cart_item_data Cart item data. * @param int $product_id Product id. * * @return array */ public function add_plan_to_cart( $cart_item_data, $product_id ) { // Read from the request so both the product-page form (POST) and direct // checkout links (`?add-to-cart=ID&subscrpt_plan_id=TERM`, GET) select a plan. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WooCommerce verifies the add-to-cart request; we only read a plan id. $plan_id = isset( $_REQUEST['subscrpt_plan_id'] ) ? absint( wp_unslash( $_REQUEST['subscrpt_plan_id'] ) ) : 0; if ( ! $plan_id ) { // Bare add-to-cart (direct link, no plan chosen): fall back to one-time // when enabled, otherwise the product's first plan. $plan_id = $this->fallback_plan_id( $product_id ); if ( ! $plan_id ) { return $cart_item_data; } } $row = $this->resolve_chosen( $product_id, $plan_id ); if ( ! $row ) { return $cart_item_data; } $terms = $this->term_terms( $row ); $cart_item_data['subscrpt_plan_id'] = $plan_id; $cart_item_data['subscrpt_plan_group_id'] = (int) $row['plan_group_id']; $cart_item_data['subscrpt_plan_price'] = $terms['price']; $cart_item_data['subscription'] = array( 'time' => $terms['time'], 'type' => $terms['option'], 'trial' => $terms['trial'], 'signup_fee' => null, 'per_cost' => $terms['price'], ); return $cart_item_data; } /** * Override the cart line price with the resolved plan price. * * @param \WC_Cart $cart Cart object. * * @return void */ public function set_cart_item_price( $cart ) { if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { return; } foreach ( $cart->get_cart() as $cart_item ) { if ( isset( $cart_item['subscrpt_plan_price'], $cart_item['data'] ) ) { $cart_item['data']->set_price( (float) $cart_item['subscrpt_plan_price'] ); } } } /** * Persist the chosen plan + terms onto the order line item so the resolved * snapshot survives past cart session (the product carries no such meta). * * @param \WC_Order_Item_Product $item Order item. * @param string $cart_item_key Cart item key. * @param array $cart_item Cart item data. * * @return void */ public function save_plan_order_item( $item, $cart_item_key, $cart_item ) { if ( empty( $cart_item['subscrpt_plan_id'] ) ) { return; } $item->update_meta_data( '_subscrpt_plan_id', (int) $cart_item['subscrpt_plan_id'] ); $item->update_meta_data( '_subscrpt_plan_group_id', (int) ( $cart_item['subscrpt_plan_group_id'] ?? 0 ) ); $item->update_meta_data( '_subscrpt_plan_price', (float) ( $cart_item['subscrpt_plan_price'] ?? 0 ) ); $item->update_meta_data( '_subscrpt_plan_terms', $cart_item['subscription'] ?? array() ); } /** * Create the subscription record from the order item's plan terms. * * Fires on `subscrpt_product_checkout` for every order item; acts only when * the item carries `_subscrpt_plan_id`. Writes the same classic snapshot meta * the engine renews on, but sourced from the plan term rather than the product. * * @param \WC_Order_Item $order_item Order item. * @param \WC_Product $product Product object. * @param string $post_status Subscription status. * * @return void */ public function create_plan_subscription( $order_item, $product, $post_status ) { $plan_id = (int) $order_item->get_meta( '_subscrpt_plan_id' ); if ( ! $plan_id ) { return; } $terms = $order_item->get_meta( '_subscrpt_plan_terms' ); $terms = is_array( $terms ) ? $terms : array(); $timing_per = (int) ( $terms['time'] ?? 1 ); $timing_option = (string) ( $terms['type'] ?? 'months' ); $trial = $terms['trial'] ?? null; $type = Helper::get_typos( $timing_per, $timing_option ); wc_update_order_item_meta( $order_item->get_id(), '_subscrpt_meta', array( 'time' => $timing_per, 'type' => $timing_option, 'trial' => $trial, ) ); $subscription_id = Helper::process_new_subscription_order( $order_item, $post_status, $product ); if ( ! $subscription_id ) { return; } update_post_meta( $subscription_id, '_subscrpt_timing_per', $timing_per ); update_post_meta( $subscription_id, '_subscrpt_timing_option', $timing_option ); update_post_meta( $subscription_id, '_subscrpt_price', (float) $order_item->get_meta( '_subscrpt_plan_price' ) ); update_post_meta( $subscription_id, '_subscrpt_plan_id', $plan_id ); update_post_meta( $subscription_id, '_subscrpt_plan_group_id', (int) $order_item->get_meta( '_subscrpt_plan_group_id' ) ); update_post_meta( $subscription_id, '_subscrpt_user_cancel', $product->get_meta( '_subscrpt_user_cancel' ) ); update_post_meta( $subscription_id, '_subscrpt_order_id', $order_item->get_order_id() ); update_post_meta( $subscription_id, '_subscrpt_order_item_id', $order_item->get_id() ); update_post_meta( $subscription_id, '_subscrpt_trial', $trial ); if ( 'active' === $post_status ) { $start_date = time(); $next_date = sdevs_wp_strtotime( $timing_per . ' ' . $type, $start_date ); if ( $trial ) { $start_date = sdevs_wp_strtotime( $trial ); $next_date = $start_date; } update_post_meta( $subscription_id, '_subscrpt_start_date', $start_date ); update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date ); } do_action( 'subscrpt_order_checkout', $subscription_id, $order_item ); } }