You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
cosmopet.ae/wp-content copy/plugins/woocommerce-subscriptions/includes/class-wcs-limited-recurring...

415 lines
14 KiB

<?php
/**
* A class for managing the limited payment recurring coupon feature.
*
* @package WooCommerce Subscriptions
* @since 4.0.0
*/
defined( 'ABSPATH' ) || exit;
class WCS_Limited_Recurring_Coupon_Manager {
/**
* The meta key used for the number of renewals.
*
* @var string
*/
private static $coupons_renewals = '_wcs_number_payments';
/**
* Initialize the class hooks and callbacks.
*/
public static function init() {
// Add custom coupon fields.
add_action( 'woocommerce_coupon_options', array( __CLASS__, 'add_coupon_fields' ), 10 );
add_action( 'woocommerce_coupon_options_save', array( __CLASS__, 'save_coupon_fields' ), 10 );
// Filter the available payment gateways.
add_filter( 'woocommerce_available_payment_gateways', array( __CLASS__, 'gateways_subscription_amount_changes' ), 20 );
// Check coupons when a subscription is renewed.
add_action( 'woocommerce_subscription_payment_complete', array( __CLASS__, 'check_coupon_usages' ) );
// Add info to the Coupons list table.
add_action( 'manage_shop_coupon_posts_custom_column', array( __CLASS__, 'add_limit_to_list_table' ), 20, 2 );
// Must be hooked later to honour early callbacks choosing to bypass the coupon removal.
add_filter( 'wcs_bypass_coupon_removal', array( __CLASS__, 'maybe_remove_coupons_from_recurring_cart' ), 1000, 5 );
}
/**
* Adds custom fields to the coupon data form.
*
* @since 4.0.0
*/
public static function add_coupon_fields( $id ) {
$coupon = new WC_Coupon( $id );
woocommerce_wp_text_input( array(
'id' => '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 '<br>';
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;
}
}