/** * A wrapper for @see wcs_get_subscriptions() which accepts simply an order ID * * @param int|WC_Order $order_id The post_id of a shop_order post or an instance of a WC_Order object * @param array $args A set of name value pairs to filter the returned value. * 'subscriptions_per_page' The number of subscriptions to return. Default set to -1 to return all. * 'offset' An optional number of subscription to displace or pass over. Default 0. * 'orderby' The field which the subscriptions should be ordered by. Can be 'start_date', 'trial_end_date', 'end_date', 'status' or 'order_id'. Defaults to 'start_date'. * 'order' The order of the values returned. Can be 'ASC' or 'DESC'. Defaults to 'DESC' * 'customer_id' The user ID of a customer on the site. * 'product_id' The post ID of a WC_Product_Subscription, WC_Product_Variable_Subscription or WC_Product_Subscription_Variation object * 'order_id' The post ID of a shop_order post/WC_Order object which was used to create the subscription * 'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'suspended', 'expired', 'pending' or 'trash'. Defaults to 'any'. * 'order_type' Get subscriptions for the any order type in this array. Can include 'any', 'parent', 'renewal' or 'switch', defaults to parent. * @return array Subscription details in post_id => WC_Subscription form. * @since 2.0 */ function wcs_get_subscriptions_for_order($order_id, $args = array()) { if (is_object($order_id)) { $order_id = $order_id->id; } $args = wp_parse_args($args, array('order_id' => $order_id, 'subscriptions_per_page' => -1, 'order_type' => array('parent', 'switch'))); // Accept either an array or string (to make it more convenient for singular types, like 'parent' or 'any') if (!is_array($args['order_type'])) { $args['order_type'] = array($args['order_type']); } $subscriptions = array(); $get_all = in_array('any', $args['order_type']) ? true : false; if ($order_id && in_array('parent', $args['order_type']) || $get_all) { $subscriptions = wcs_get_subscriptions($args); } if (wcs_order_contains_resubscribe($order_id) && (in_array('resubscribe', $args['order_type']) || $get_all)) { $subscriptions += wcs_get_subscriptions_for_resubscribe_order($order_id); } if (wcs_order_contains_renewal($order_id) && (in_array('renewal', $args['order_type']) || $get_all)) { $subscriptions += wcs_get_subscriptions_for_renewal_order($order_id); } if (wcs_order_contains_switch($order_id) && (in_array('switch', $args['order_type']) || $get_all)) { $subscriptions += wcs_get_subscriptions_for_switch_order($order_id); } return $subscriptions; }
/** * Updates other subscription sources. */ protected function save_source($order, $source) { parent::save_source($order, $source); // Also store it on the subscriptions being purchased or paid for in the order if (wcs_order_contains_subscription($order->id)) { $subscriptions = wcs_get_subscriptions_for_order($order->id); } elseif (wcs_order_contains_renewal($order->id)) { $subscriptions = wcs_get_subscriptions_for_renewal_order($order->id); } else { $subscriptions = array(); } foreach ($subscriptions as $subscription) { update_post_meta($subscription->id, '_stripe_customer_id', $source->customer); update_post_meta($subscription->id, '_stripe_card_id', $source->source); } }
/** * Display a notice if functions are hooked to the old filter and apply the old filters args * * @since 2.0 */ protected function trigger_hook($old_hook, $new_callback_args) { if (0 === strpos($old_hook, 'admin_changed_subscription_to_')) { // New arg spec: $subscription_id // Old arg spec: $subscription_key $subscription = wcs_get_subscription($new_callback_args[0]); do_action($old_hook, wcs_get_old_subscription_key($subscription)); } elseif (0 === strpos($old_hook, 'scheduled_subscription_payment_')) { // New arg spec: $amount, $renewal_order // Old arg spec: $amount, $original_order, $product_id $subscription = $new_callback_args[0]; $subscriptions = wcs_get_subscriptions_for_renewal_order($new_callback_args[1]); if (!empty($subscriptions)) { $subscription = array_pop($subscriptions); do_action($old_hook, $new_callback_args[0], self::get_order($subscription), self::get_product_id($subscription)); } } elseif (0 === strpos($old_hook, 'activated_subscription_') || 0 === strpos($old_hook, 'reactivated_subscription_') || 0 === strpos($old_hook, 'subscription_put_on-hold_') || 0 === strpos($old_hook, 'cancelled_subscription_') || 0 === strpos($old_hook, 'subscription_expired_')) { // New arg spec: $subscription // Old arg spec: $order, $product_id $subscription = $new_callback_args[0]; do_action($old_hook, self::get_order($subscription), self::get_product_id($subscription)); } elseif (0 === strpos($old_hook, 'customer_changed_subscription_to_')) { // New arg spec: $subscription // Old arg spec: $subscription_key do_action($old_hook, wcs_get_old_subscription_key($new_callback_args[0])); } elseif (0 === strpos($old_hook, 'woocommerce_subscriptions_updated_recurring_payment_method_to_')) { // New arg spec: $subscription, $old_payment_method // Old arg spec: $order, $subscription_key, $old_payment_method $subscription = $new_callback_args[0]; $old_payment_method = $new_callback_args[2]; do_action($old_hook, self::get_order($subscription), wcs_get_old_subscription_key($subscription), $old_payment_method); } elseif (0 === strpos($old_hook, 'woocommerce_subscriptions_updated_recurring_payment_method_from_')) { // New arg spec: $subscription, $new_payment_method // Old arg spec: $order, $subscription_key, $new_payment_method $subscription = $new_callback_args[0]; $new_payment_method = $new_callback_args[1]; do_action($old_hook, self::get_order($subscription), wcs_get_old_subscription_key($subscription), $new_payment_method); } elseif (0 === strpos($old_hook, 'woocommerce_subscriptions_changed_failing_payment_method_')) { // New arg spec: $subscription, $renewal_order // Old arg spec: $original_order, $renewal_order, $subscription_key $subscription = $new_callback_args[0]; do_action($old_hook, self::get_order($subscription), $new_callback_args[1], wcs_get_old_subscription_key($subscription)); } }
/** * Process subscription renewal * * @since 1.4 * @param float $amount_to_charge subscription amount to charge, could include * multiple renewals if they've previously failed and the admin * has enabled it * @param WC_Order $order original order containing the subscription * @param int $product_id the ID of the subscription product */ public function process_renewal_payment($amount_to_charge, $order, $product_id = null) { require_once 'class-wc-realex-api.php'; $realex_subscription_count = 0; if (is_numeric($order->realex_subscription_count) && $order->realex_subscription_count) { $realex_subscription_count = $order->realex_subscription_count; } // increment the subscription count so we don't get order number clashes $realex_subscription_count++; update_post_meta($order->id, '_realex_subscription_count', $realex_subscription_count); // set custom class member used by the realex gateway $order->payment_total = SV_WC_Helper::number_format($amount_to_charge); // zero-dollar subscription renewal. weird, but apparently it happens -- only applicable to Subs 1.5.x if (!SV_WC_Plugin_Compatibility::is_wc_subscriptions_version_gte_2_0()) { if (0 == $order->payment_total) { // add order note $order->add_order_note(sprintf(__('%s0 Subscription Renewal Approved', 'woocommerce-gateway-realex'), get_woocommerce_currency_symbol())); // update subscription WC_Subscriptions_Manager::process_subscription_payments_on_order($order, $product_id); return; } } // This order is missing a tokenized card, lets see whether there's one available for the customer if (!get_post_meta($order->id, '_realex_cardref', true)) { $credit_cards = get_user_meta($order->get_user_id(), 'woocommerce_realex_cc', true); if (is_array($credit_cards)) { $card_ref = (object) current($credit_cards); $card_ref = $card_ref->ref; update_post_meta($order->id, '_realex_cardref', $card_ref); if (SV_WC_Plugin_Compatibility::is_wc_subscriptions_version_gte_2_0()) { foreach (wcs_get_subscriptions_for_renewal_order($order) as $subscription) { update_post_meta($subscription->id, '_realex_cardref', $card_ref); } } } } // create the realex api client $realex_client = new Realex_API($this->get_endpoint_url(), $this->get_realvault_endpoint_url(), $this->get_shared_secret()); // create the customer/cc tokens, and authorize the initial payment amount, if any $response = $this->authorize($realex_client, $order); if ($response && '00' == $response->result) { // add order note $order->add_order_note(sprintf(__('Credit Card Subscription Renewal Payment Approved (Payment Reference: %s) ', 'woocommerce-gateway-realex'), $response->pasref)); // update subscription if (SV_WC_Plugin_Compatibility::is_wc_subscriptions_version_gte_2_0()) { $order->payment_complete((string) $response->pasref); } else { WC_Subscriptions_Manager::process_subscription_payments_on_order($order, $product_id); } } else { // generate the result message $message = __('Credit Card Subscription Renewal Payment Failed', 'woocommerce-gateway-realex'); /* translators: Placeholders: %1$s - result, %2$s - result message */ if ($response) { $message .= sprintf(__(' (Result: %1$s - "%2$s").', 'woocommerce-gateway-realex'), $response->result, $response->message); } $order->add_order_note($message); // update subscription if (!SV_WC_Plugin_Compatibility::is_wc_subscriptions_version_gte_2_0()) { WC_Subscriptions_Manager::process_subscription_payment_failure_on_order($order, $product_id); } } }
/** * Get PayPal Args for passing to PP * * Based on the HTML Variables documented here: https://developer.paypal.com/webapps/developer/docs/classic/paypal-payments-standard/integration-guide/Appx_websitestandard_htmlvariables/#id08A6HI00JQU * * @param WC_Order $order * @return array */ public static function get_paypal_args($paypal_args, $order) { $is_payment_change = WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment; $order_contains_failed_renewal = false; // Payment method changes act on the subscription not the original order if ($is_payment_change) { $subscriptions = array(wcs_get_subscription($order->id)); $subscription = array_pop($subscriptions); $order = $subscription->order; // We need the subscription's total remove_filter('woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2); } else { // Otherwise the order is the $order if ($cart_item = wcs_cart_contains_failed_renewal_order_payment() || false !== WC_Subscriptions_Renewal_Order::get_failed_order_replaced_by($order->id)) { $subscriptions = wcs_get_subscriptions_for_renewal_order($order); $order_contains_failed_renewal = true; } else { $subscriptions = wcs_get_subscriptions_for_order($order); } // Only one subscription allowed per order with PayPal $subscription = array_pop($subscriptions); } if ($order_contains_failed_renewal || !empty($subscription) && $subscription->get_total() > 0 && 'yes' !== get_option(WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no')) { // It's a subscription $paypal_args['cmd'] = '_xclick-subscriptions'; // Store the subscription ID in the args sent to PayPal so we can access them later $paypal_args['custom'] = wcs_json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key, 'subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key)); foreach ($subscription->get_items() as $item) { if ($item['qty'] > 1) { $item_names[] = $item['qty'] . ' x ' . wcs_get_paypal_item_name($item['name']); } elseif ($item['qty'] > 0) { $item_names[] = wcs_get_paypal_item_name($item['name']); } } // translators: 1$: subscription ID, 2$: order ID, 3$: names of items, comma separated $paypal_args['item_name'] = wcs_get_paypal_item_name(sprintf(_x('Subscription %1$s (Order %2$s) - %3$s', 'item name sent to paypal', 'woocommerce-subscriptions'), $subscription->get_order_number(), $order->get_order_number(), implode(', ', $item_names))); $unconverted_periods = array('billing_period' => $subscription->billing_period, 'trial_period' => $subscription->trial_period); $converted_periods = array(); // Convert period strings into PayPay's format foreach ($unconverted_periods as $key => $period) { switch (strtolower($period)) { case 'day': $converted_periods[$key] = 'D'; break; case 'week': $converted_periods[$key] = 'W'; break; case 'year': $converted_periods[$key] = 'Y'; break; case 'month': default: $converted_periods[$key] = 'M'; break; } } $price_per_period = $subscription->get_total(); $subscription_interval = $subscription->billing_interval; $start_timestamp = $subscription->get_time('start'); $trial_end_timestamp = $subscription->get_time('trial_end'); $next_payment_timestamp = $subscription->get_time('next_payment'); $is_synced_subscription = WC_Subscriptions_Synchroniser::subscription_contains_synced_product($subscription->id); if ($is_synced_subscription) { $length_from_timestamp = $next_payment_timestamp; } elseif ($trial_end_timestamp > 0) { $length_from_timestamp = $trial_end_timestamp; } else { $length_from_timestamp = $start_timestamp; } $subscription_length = wcs_estimate_periods_between($length_from_timestamp, $subscription->get_time('end'), $subscription->billing_period); $subscription_installments = $subscription_length / $subscription_interval; $initial_payment = $is_payment_change ? 0 : $order->get_total(); if ($order_contains_failed_renewal || $is_payment_change) { if ($is_payment_change) { // Add a nonce to the order ID to avoid "This invoice has already been paid" error when changing payment method to PayPal when it was previously PayPal $suffix = '-wcscpm-' . wp_create_nonce(); } else { // Failed renewal order, append a descriptor and renewal order's ID $suffix = '-wcsfrp-' . $order->id; } // Change the 'invoice' and the 'custom' values to be for the original order (if there is one) if (false === $subscription->order) { // No original order so we need to use the subscriptions values instead $order_number = ltrim($subscription->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions')) . '-subscription'; $order_id_key = array('order_id' => $subscription->id, 'order_key' => $subscription->order_key); } else { $order_number = ltrim($subscription->order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions')); $order_id_key = array('order_id' => $subscription->order->id, 'order_key' => $subscription->order->order_key); } $order_details = false !== $subscription->order ? $subscription->order : $subscription; // Set the invoice details to the original order's invoice but also append a special string and this renewal orders ID so that we can match it up as a failed renewal order payment later $paypal_args['invoice'] = WCS_PayPal::get_option('invoice_prefix') . $order_number . $suffix; $paypal_args['custom'] = wcs_json_encode(array_merge($order_id_key, array('subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key))); } if ($order_contains_failed_renewal) { $subscription_trial_length = 0; $subscription_installments = max($subscription_installments - $subscription->get_completed_payment_count(), 0); // If we're changing the payment date or switching subs, we need to set the trial period to the next payment date & installments to be the number of installments left } elseif ($is_payment_change || $is_synced_subscription) { $next_payment_timestamp = $subscription->get_time('next_payment'); // When the subscription is on hold if (false != $next_payment_timestamp && !empty($next_payment_timestamp)) { $trial_until = wcs_calculate_paypal_trial_periods_until($next_payment_timestamp); $subscription_trial_length = $trial_until['first_trial_length']; $converted_periods['trial_period'] = $trial_until['first_trial_period']; $second_trial_length = $trial_until['second_trial_length']; $second_trial_period = $trial_until['second_trial_period']; } else { $subscription_trial_length = 0; } // If this is a payment change, we need to account for completed payments on the number of installments owing if ($is_payment_change && $subscription_length > 0) { $subscription_installments = max($subscription_installments - $subscription->get_completed_payment_count(), 0); } } else { $subscription_trial_length = wcs_estimate_periods_between($start_timestamp, $trial_end_timestamp, $subscription->trial_period); } if ($subscription_trial_length > 0) { // Specify a free trial period $paypal_args['a1'] = $initial_payment > 0 ? $initial_payment : 0; // Trial period length $paypal_args['p1'] = $subscription_trial_length; // Trial period $paypal_args['t1'] = $converted_periods['trial_period']; // We need to use a second trial period before we have more than 90 days until the next payment if (isset($second_trial_length) && $second_trial_length > 0) { $paypal_args['a2'] = 0.01; // Alas, although it's undocumented, PayPal appears to require a non-zero value in order to allow a second trial period $paypal_args['p2'] = $second_trial_length; $paypal_args['t2'] = $second_trial_period; } } elseif ($initial_payment != $price_per_period) { // No trial period, but initial amount includes a sign-up fee and/or other items, so charge it as a separate period if (1 == $subscription_installments) { $param_number = 3; } else { $param_number = 1; } $paypal_args['a' . $param_number] = $initial_payment; // Sign Up interval $paypal_args['p' . $param_number] = $subscription_interval; // Sign Up unit of duration $paypal_args['t' . $param_number] = $converted_periods['billing_period']; } // We have a recurring payment if (!isset($param_number) || 1 == $param_number) { // Subscription price $paypal_args['a3'] = $price_per_period; // Subscription duration $paypal_args['p3'] = $subscription_interval; // Subscription period $paypal_args['t3'] = $converted_periods['billing_period']; } // Recurring payments if (1 == $subscription_installments || $initial_payment != $price_per_period && 0 == $subscription_trial_length && 2 == $subscription_installments) { // Non-recurring payments $paypal_args['src'] = 0; } else { $paypal_args['src'] = 1; if ($subscription_installments > 0) { // An initial period is being used to charge a sign-up fee if ($initial_payment != $price_per_period && 0 == $subscription_trial_length) { $subscription_installments--; } $paypal_args['srt'] = $subscription_installments; } } // Don't reattempt failed payments, instead let Subscriptions handle the failed payment $paypal_args['sra'] = 0; // Force return URL so that order description & instructions display $paypal_args['rm'] = 2; // Reattach the filter we removed earlier if ($is_payment_change) { add_filter('woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2); } } return $paypal_args; }
/** * If the payment for a renewal order has previously failed and is then paid, we need to make sure the * subscription payment function is called. * * @param int $user_id The id of the user who purchased the subscription * @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key() * @since 1.2 * @deprecated 2.0 */ public static function process_subscription_payment_on_child_order($order_id, $payment_status = 'completed') { _deprecated_function(__METHOD__, '2.0'); if (wcs_order_contains_renewal($order_id)) { $subscriptions = wcs_get_subscriptions_for_renewal_order($order_id); foreach ($subscriptions as $subscription) { if ('failed' == $payment_status) { $subscription->payment_failed(); } else { $subscription->payment_complete(); $subscription->update_status('active'); } } } }
/** * Customise which actions are shown against a subscription renewal order on the My Account page. * * @since 2.0 */ public function filter_my_account_my_orders_actions($actions, $order) { if (wcs_order_contains_renewal($order)) { unset($actions['cancel']); // If the subscription has been deleted or reactivated some other way, don't support payment on the order $subscriptions = wcs_get_subscriptions_for_renewal_order($order); foreach ($subscriptions as $subscription) { if (empty($subscription) || !$subscription->has_status(array('on-hold', 'pending'))) { unset($actions['pay']); break; } } } return $actions; }