'wcs_number_payments', 'label' => __( 'Active for x payments', 'woocommerce-subscriptions' ), 'placeholder' => __( 'Unlimited payments', 'woocommerce-subscriptions' ), 'description' => __( 'Coupon will be limited to the given number of payments. It will then be automatically removed from the subscription. "Payments" also includes the initial subscription payment.', 'woocommerce-subscriptions' ), 'desc_tip' => true, 'data_type' => 'decimal', 'value' => $coupon->get_meta( self::$coupons_renewals ), ) ); } /** * Saves our custom coupon fields. * * @since 4.0.0 * @param int $id The coupon's ID. */ public static function save_coupon_fields( $id ) { // Check the nonce (again). if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ), 'woocommerce_save_data' ) ) { return; } $coupon = new WC_Coupon( $id ); $coupon->add_meta_data( self::$coupons_renewals, wc_clean( $_POST['wcs_number_payments'] ), true ); $coupon->save(); } /** * Get the number of renewals for a limited coupon. * * @since 4.0.0 * @param string $code The coupon code. * @return false|int False for non-recurring coupons, or the limit number for recurring coupons. * A value of 0 is for unlimited usage. */ public static function get_coupon_limit( $code ) { if ( wcs_is_woocommerce_pre( '3.2' ) ) { return false; } // Retrieve the coupon data. $coupon = new WC_Coupon( $code ); $coupon_type = $coupon->get_discount_type(); // If we have a virtual coupon, attempt to get the original coupon. if ( WC_Subscriptions_Coupon::is_renewal_cart_coupon( $coupon_type ) ) { $coupon = WC_Subscriptions_Coupon::map_virtual_coupon( $code ); $coupon_type = $coupon->get_discount_type(); } $limited = $coupon->get_meta( self::$coupons_renewals ); return WC_Subscriptions_Coupon::is_recurring_coupon( $coupon_type ) ? intval( $limited ) : false; } /** * Determines if a given coupon is limited to a certain number of renewals. * * @since 4.0.0 * * @param string $code The coupon code. * @return bool */ public static function coupon_is_limited( $code ) { return (bool) self::get_coupon_limit( $code ); } /** * Determines whether the cart contains a recurring coupon with set number of renewals. * * @since 4.0.0 * @return bool Whether the cart contains a limited recurring coupon. */ public static function cart_contains_limited_recurring_coupon() { $has_coupon = false; $applied_coupons = isset( WC()->cart->applied_coupons ) ? WC()->cart->applied_coupons : array(); foreach ( $applied_coupons as $code ) { if ( self::coupon_is_limited( $code ) ) { $has_coupon = true; break; } } return $has_coupon; } /** * Determines if a given order has a limited use coupon. * * @since 4.0.0 * @param WC_Order|WC_Subscription $order * * @return bool Whether the order contains a limited recurring coupon. */ public static function order_has_limited_recurring_coupon( $order ) { $has_coupon = false; foreach ( wcs_get_used_coupon_codes( $order ) as $code ) { if ( self::coupon_is_limited( $code ) ) { $has_coupon = true; break; } } return $has_coupon; } /** * Limits payment gateways to those that support changing subscription amounts. * * @since 4.0.0 * @param WC_Payment_Gateway[] $gateways The current available gateways. * @return WC_Payment_Gateway[] */ private static function limit_gateways_subscription_amount_changes( $gateways ) { foreach ( $gateways as $index => $gateway ) { if ( $gateway->supports( 'subscriptions' ) && ! $gateway->supports( 'subscription_amount_changes' ) ) { unset( $gateways[ $index ] ); } } return $gateways; } /** * Determines how many subscription renewals the coupon has been applied to and removes coupons which have reached their expiry. * * @since 4.0.0 * @param WC_Subscription $subscription The current subscription. */ public static function check_coupon_usages( $subscription ) { // If there aren't any coupons, there's nothing to do. $coupons = wcs_get_used_coupon_codes( $subscription ); if ( empty( $coupons ) ) { return; } // Set up the coupons we're looking for, and an initial count. $limited_coupons = array(); foreach ( $coupons as $code ) { if ( self::coupon_is_limited( $code ) ) { $limited_coupons[ $code ] = array( 'code' => $code, 'count' => 0, ); } } // Don't continue if we have no limited use coupons. if ( empty( $limited_coupons ) ) { return; } // Get all related orders, and count the number of uses for each coupon. $related = $subscription->get_related_orders( 'all' ); /** @var WC_Order $order */ foreach ( $related as $id => $order ) { // Unpaid orders don't count as usages. if ( $order->needs_payment() ) { continue; } /* * If the order has been refunded, treat coupon as unused. We'll consider the order to be * refunded when there is a non-null refund amount, and the order total equals the refund amount. * * The use of == instead of === is deliberate, to account for differences in amount formatting. */ $refunded = $order->get_total_refunded(); $total = $order->get_total(); if ( $refunded && $total == $refunded ) { continue; } // If there was nothing discounted, then consider the coupon unused. if ( ! $order->get_discount_total() ) { continue; } // Check for limited coupons, and add them to the count if the provide a discount. $used_coupons = $order->get_items( 'coupon' ); /** @var WC_Order_Item_Coupon $used_coupon */ foreach ( $used_coupons as $used_coupon ) { if ( isset( $limited_coupons[ $used_coupon->get_code() ] ) && $used_coupon->get_discount() ) { $limited_coupons[ $used_coupon->get_code() ]['count']++; } } } // Check each coupon to see if it needs to be removed. foreach ( $limited_coupons as $limited_coupon ) { if ( self::get_coupon_limit( $limited_coupon['code'] ) <= $limited_coupon['count'] ) { $subscription->remove_coupon( $limited_coupon['code'] ); $subscription->add_order_note( sprintf( /* translators: %1$s is the coupon code, %2$d is the number of payment usages */ _n( 'Limited use coupon "%1$s" removed from subscription. It has been used %2$d time.', 'Limited use coupon "%1$s" removed from subscription. It has been used %2$d times.', $limited_coupon['count'], 'woocommerce-subscriptions' ), $limited_coupon['code'], number_format_i18n( $limited_coupon['count'] ) ) ); } } } /** * Add our limited coupon data to the Coupon list table. * * @since 4.0.0 * * @param string $column_name The name of the current column in the table. * @param int $id The coupon ID. */ public static function add_limit_to_list_table( $column_name, $id ) { if ( 'usage' !== $column_name ) { return; } $limit = self::get_coupon_limit( wc_get_coupon_code_by_id( $id ) ); if ( false === $limit ) { return; } echo '
'; if ( $limit ) { echo esc_html( sprintf( /* translators: %d refers to the number of payments the coupon can be used for. */ _n( 'Active for %d payment', 'Active for %d payments', $limit, 'woocommerce-subscriptions' ), number_format_i18n( $limit ) ) ); } else { esc_html_e( 'Active for unlimited payments', 'woocommerce-subscriptions' ); } } /** * Determines if a given recurring cart contains a limited use coupon which when applied to a subscription will reach its usage limit within the subscription's length. * * @since 4.0.0 * * @param WC_Cart $recurring_cart The recurring cart object. * @return bool */ public static function recurring_cart_contains_expiring_coupon( $recurring_cart ) { $limited_recurring_coupons = array(); if ( isset( $recurring_cart->applied_coupons ) ) { $limited_recurring_coupons = array_filter( $recurring_cart->applied_coupons, array( __CLASS__, 'coupon_is_limited' ) ); } // Bail early if there are no limited coupons applied to the recurring cart or if there is no discount provided. if ( empty( $limited_recurring_coupons ) || ! $recurring_cart->discount_cart ) { return false; } $has_expiring_coupon = false; $subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' ); $subscription_payments = $subscription_length / wcs_cart_pluck( $recurring_cart, 'subscription_period_interval' ); // Limited recurring coupons will always expire at some point on subscriptions with no length. if ( empty( $subscription_length ) ) { $has_expiring_coupon = true; } else { foreach ( $limited_recurring_coupons as $code ) { if ( WC_Subscriptions_Coupon::get_coupon_limit( $code ) < $subscription_payments ) { $has_expiring_coupon = true; break; } } } return $has_expiring_coupon; } /** * Filters the available gateways when there is a recurring coupon. * * @since 4.0.0 * * @param WC_Payment_Gateway[] $gateways The available payment gateways. * @return WC_Payment_Gateway[] The filtered payment gateways. */ public static function gateways_subscription_amount_changes( $gateways ) { // If there are already no gateways or we're on the order-pay screen, bail early. if ( empty( $gateways ) || is_wc_endpoint_url( 'order-pay' ) ) { return $gateways; } // See if this is a request to change payment for an existing subscription. $change_payment = isset( $_GET['change_payment_method'] ) ? wc_clean( $_GET['change_payment_method'] ) : 0; $has_limited_coupon = false; if ( $change_payment && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) ) ) { $subscription = wcs_get_subscription( $change_payment ); $has_limited_coupon = self::order_has_limited_recurring_coupon( $subscription ); } // If the cart doesn't have a limited coupon, and a change payment doesn't have a limited coupon, bail early. if ( ! self::cart_contains_limited_recurring_coupon() && ! $has_limited_coupon ) { return $gateways; } // If we got this far, we should limit the gateways as needed. $gateways = self::limit_gateways_subscription_amount_changes( $gateways ); // If there are no gateways now, it's because of the coupon. Filter the 'no available payment methods' message. if ( empty( $gateways ) ) { add_filter( 'woocommerce_no_available_payment_methods_message', array( __CLASS__, 'no_available_payment_methods_message' ), 20 ); } return $gateways; } /** * Filter the message for when no payment gateways are available. * * @since 4.0.0 * * @param string $message The current message indicating there are no payment methods available.. * @return string The filtered message indicating there are no payment methods available. */ public static function no_available_payment_methods_message() { return __( 'Sorry, it seems there are no available payment methods which support the recurring coupon you are using. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce-subscriptions' ); } /** * Removes limited coupons from the recurring cart if the coupons limit is reached in the initial cart. * * @since 4.0.0 * * @param bool $bypass_default_checks Whether to bypass WC Subscriptions default conditions for removing a coupon. * @param WC_Coupon $coupon The coupon to check. * @param string $coupon_type The coupon's type. * @param string $calculation_type The WC Subscriptions cart calculation mode. Can be 'recurring_total' or 'none'. @see WC_Subscriptions_Cart::get_calculation_type() * * @return bool Whether to bypass WC Subscriptions default conditions for removing a coupon. */ public static function maybe_remove_coupons_from_recurring_cart( $bypass_default_checks, $coupon, $coupon_type, $calculation_type, $cart ) { // Bypass this check if a third-party has already opted to bypass default conditions. if ( $bypass_default_checks ) { return $bypass_default_checks; } if ( 'recurring_total' !== $calculation_type ) { return $bypass_default_checks; } if ( ! WC_Subscriptions_Coupon::is_recurring_coupon( $coupon_type ) ) { return $bypass_default_checks; } // Special handling for a single payment coupon. if ( 1 === self::get_coupon_limit( $coupon->get_code() ) && 0 < WC()->cart->get_coupon_discount_amount( $coupon->get_code() ) ) { $cart->remove_coupon( $coupon->get_code() ); } return $bypass_default_checks; } }