1161 lines
42 KiB
PHP
1161 lines
42 KiB
PHP
<?php
|
|
/**
|
|
* Class WC_Payments_Subscription_Service
|
|
*
|
|
* @package WooCommerce\Payments
|
|
*/
|
|
|
|
use WCPay\Constants\Order_Mode;
|
|
use WCPay\Exceptions\API_Exception;
|
|
use WCPay\Exceptions\Amount_Too_Small_Exception;
|
|
use WCPay\Exceptions\Cannot_Combine_Currencies_Exception;
|
|
use WCPay\Exceptions\Subscription_Mode_Mismatch_Exception;
|
|
use WCPay\Logger;
|
|
|
|
/**
|
|
* Subscriptions logic for WCPay Subscriptions
|
|
*/
|
|
class WC_Payments_Subscription_Service {
|
|
|
|
use WC_Payments_Subscriptions_Utilities;
|
|
|
|
/**
|
|
* WCPay subscriptions endpoint on server.
|
|
*
|
|
* @const string
|
|
*/
|
|
const SUBSCRIPTION_API_PATH = '/subscriptions';
|
|
|
|
/**
|
|
* Subscription meta key used to store WCPay subscription's ID.
|
|
*
|
|
* @const string
|
|
*/
|
|
const SUBSCRIPTION_ID_META_KEY = '_wcpay_subscription_id';
|
|
|
|
/**
|
|
* Subscription item meta key used to store WCPay subscription item's ID.
|
|
*
|
|
* @const string
|
|
*/
|
|
const SUBSCRIPTION_ITEM_ID_META_KEY = '_wcpay_subscription_item_id';
|
|
|
|
/**
|
|
* Subscription discounts meta key used to store WCPay subscription discount IDs.
|
|
*
|
|
* @const string
|
|
*/
|
|
const SUBSCRIPTION_DISCOUNT_IDS_META_KEY = '_wcpay_subscription_discount_ids';
|
|
|
|
/**
|
|
* WC Payments API Client
|
|
*
|
|
* @var WC_Payments_API_Client
|
|
*/
|
|
private $payments_api_client;
|
|
|
|
/**
|
|
* Customer Service
|
|
*
|
|
* @var WC_Payments_Customer_Service
|
|
*/
|
|
private $customer_service;
|
|
|
|
/**
|
|
* Product Service
|
|
*
|
|
* @var WC_Payments_Product_Service
|
|
*/
|
|
private $product_service;
|
|
|
|
/**
|
|
* Invoice Service
|
|
*
|
|
* @var WC_Payments_Invoice_Service
|
|
*/
|
|
private $invoice_service;
|
|
|
|
/**
|
|
* The features WCPay Subscriptions Support.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $supports = [
|
|
'gateway_scheduled_payments',
|
|
'multiple_subscriptions',
|
|
'subscription_cancellation',
|
|
'subscription_payment_method_change_admin',
|
|
'subscription_payment_method_change_customer',
|
|
'subscription_payment_method_change',
|
|
'subscription_reactivation',
|
|
'subscription_suspension',
|
|
'subscriptions',
|
|
];
|
|
|
|
/**
|
|
* A set of temporary exceptions to the limited feature support.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $feature_support_exceptions = [];
|
|
|
|
/**
|
|
* Whether the current request is creating a WCPay subscription when
|
|
* updating the subscription payment method from the "My account" page.
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $is_creating_subscription_from_update_payment_method = false;
|
|
|
|
/**
|
|
* WC Payments Subscriptions Constructor.
|
|
*
|
|
* Attaches callbacks for managing WC Subscriptions.
|
|
*
|
|
* @param WC_Payments_API_Client $api_client WC payments API Client.
|
|
* @param WC_Payments_Customer_Service $customer_service WC payments customer service.
|
|
* @param WC_Payments_Product_Service $product_service WC payments Products service.
|
|
* @param WC_Payments_Invoice_Service $invoice_service WC payments Invoice service.
|
|
*/
|
|
public function __construct(
|
|
WC_Payments_API_Client $api_client,
|
|
WC_Payments_Customer_Service $customer_service,
|
|
WC_Payments_Product_Service $product_service,
|
|
WC_Payments_Invoice_Service $invoice_service
|
|
) {
|
|
|
|
$this->payments_api_client = $api_client;
|
|
$this->customer_service = $customer_service;
|
|
$this->product_service = $product_service;
|
|
$this->invoice_service = $invoice_service;
|
|
|
|
/**
|
|
* When a store is in staging mode, we don't want any subscription updates or purchases to be sent to the server.
|
|
*
|
|
* Sending these requests from staging sites can have unintended consequences for the live store. For example,
|
|
* Subscriptions which renew on the staging site will lead to pausing the shared subscription record at Stripe
|
|
* and that will result in inexplicable paused subscriptions and missed renewal payments for the live site.
|
|
*/
|
|
if ( WC_Payments_Subscriptions::is_duplicate_site() ) {
|
|
return;
|
|
}
|
|
|
|
if ( WC_Payments_Features::should_use_stripe_billing() ) {
|
|
add_action( 'woocommerce_checkout_subscription_created', [ $this, 'create_subscription' ] );
|
|
add_action( 'woocommerce_renewal_order_payment_complete', [ $this, 'create_subscription_for_manual_renewal' ] );
|
|
add_action( 'woocommerce_subscription_payment_method_updated', [ $this, 'maybe_create_subscription_from_update_payment_method' ], 10, 2 );
|
|
}
|
|
|
|
if ( class_exists( 'WC_Subscription' ) ) {
|
|
// Save the new token on the WCPay subscription when it's added to a WC subscription.
|
|
add_action( 'woocommerce_payment_token_added_to_order', [ $this, 'update_wcpay_subscription_payment_method' ], 10, 3 );
|
|
|
|
add_action( 'woocommerce_subscription_status_cancelled', [ $this, 'cancel_subscription' ] );
|
|
add_action( 'woocommerce_subscription_status_expired', [ $this, 'cancel_subscription' ] );
|
|
add_action( 'woocommerce_subscription_status_on-hold', [ $this, 'handle_subscription_status_on_hold' ] );
|
|
add_action( 'woocommerce_subscription_status_pending-cancel', [ $this, 'set_pending_cancel_for_subscription' ] );
|
|
add_action( 'woocommerce_subscription_status_pending-cancel_to_active', [ $this, 'reactivate_subscription' ] );
|
|
add_action( 'woocommerce_subscription_status_on-hold_to_active', [ $this, 'reactivate_subscription' ] );
|
|
|
|
add_filter( 'woocommerce_subscription_payment_gateway_supports', [ $this, 'prevent_wcpay_subscription_changes' ], 10, 3 );
|
|
add_filter( 'woocommerce_order_actions', [ $this, 'prevent_wcpay_manual_renewal' ], 11, 1 );
|
|
|
|
add_action( 'woocommerce_payments_changed_subscription_payment_method', [ $this, 'maybe_attempt_payment_for_subscription' ], 10, 2 );
|
|
add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'show_wcpay_subscription_id' ] );
|
|
|
|
add_action( 'woocommerce_subscription_payment_method_updated_from_' . WC_Payment_Gateway_WCPay::GATEWAY_ID, [ $this, 'maybe_cancel_subscription' ], 10, 2 );
|
|
|
|
add_action( 'wcs_renewal_order_items', [ $this, 'check_wcpay_mode_for_subscription' ], 10, 3 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the WC subscription has a first payment date that is in the future.
|
|
*
|
|
* @param WC_Subscription $subscription WC subscription to check if first payment is now or delayed.
|
|
*
|
|
* @return bool Whether the first payment is delayed.
|
|
*/
|
|
public static function has_delayed_payment( WC_Subscription $subscription ) {
|
|
$trial_end = $subscription->get_time( 'trial_end' );
|
|
$has_sync = false;
|
|
|
|
if ( ! class_exists( 'WC_Subscriptions_Synchroniser' ) ) {
|
|
return $has_sync;
|
|
}
|
|
|
|
if ( WC_Subscriptions_Synchroniser::is_syncing_enabled() && WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription ) ) {
|
|
$has_sync = true;
|
|
|
|
foreach ( $subscription->get_items() as $item ) {
|
|
$synced_payment_date = WC_Subscriptions_Synchroniser::calculate_first_payment_date( $item->get_product(), 'timestamp' );
|
|
|
|
// Check if the subscription starts from today because in those cases we don't need a dynamic trial period to align to the payment date.
|
|
if ( WC_Subscriptions_Synchroniser::is_today( $synced_payment_date ) ) {
|
|
$has_sync = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $has_sync || $trial_end > time();
|
|
}
|
|
|
|
/**
|
|
* Gets the WC subscription associated with a WCPay subscription ID.
|
|
*
|
|
* @param string $wcpay_subscription_id WCPay subscription ID.
|
|
*
|
|
* @return WC_Subscription|bool The WC subscription or false if it can't be found.
|
|
*/
|
|
public static function get_subscription_from_wcpay_subscription_id( string $wcpay_subscription_id ) {
|
|
if ( ! function_exists( 'wcs_get_subscriptions' ) ) {
|
|
return false;
|
|
}
|
|
|
|
$subscriptions = wcs_get_subscriptions(
|
|
[
|
|
'subscriptions_per_page' => 1,
|
|
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
[
|
|
'key' => self::SUBSCRIPTION_ID_META_KEY,
|
|
'value' => $wcpay_subscription_id,
|
|
],
|
|
],
|
|
]
|
|
);
|
|
|
|
return empty( $subscriptions ) ? false : reset( $subscriptions );
|
|
}
|
|
|
|
/**
|
|
* Gets the WCPay subscription ID from a WC subscription.
|
|
*
|
|
* @param WC_Subscription $subscription WC Subscription.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_wcpay_subscription_id( WC_Subscription $subscription ) {
|
|
return $subscription->get_meta( self::SUBSCRIPTION_ID_META_KEY, true );
|
|
}
|
|
|
|
/**
|
|
* Gets the WCPay subscription item ID from a WC subscription item.
|
|
*
|
|
* @param WC_Order_Item $item WC Item.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_wcpay_subscription_item_id( WC_Order_Item $item ) {
|
|
return $item->get_meta( self::SUBSCRIPTION_ITEM_ID_META_KEY, true );
|
|
}
|
|
|
|
/**
|
|
* Gets the WCPay subscription discount IDs from a WC subscription.
|
|
*
|
|
* @param WC_Subscription $subscription WC Subscription.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_wcpay_discount_ids( WC_Subscription $subscription ) {
|
|
return $subscription->get_meta( self::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, true );
|
|
}
|
|
|
|
/**
|
|
* Sets Stripe discount ids on WC subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC Subscription object.
|
|
* @param array $discounts The WCPay discount data.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function set_wcpay_discount_ids( WC_Subscription $subscription, array $discounts ) {
|
|
$subscription->update_meta_data( self::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, $discounts );
|
|
$subscription->save();
|
|
}
|
|
|
|
/**
|
|
* Determines if a given WC subscription is a WCPay subscription.
|
|
*
|
|
* On duplicate sites (staging or dev environments) all WCPay Subscrptions are disabled and so return false.
|
|
* This is to avoid dev environments interacting with WCPay Subscriptions and communicating on behalf of the live store.
|
|
*
|
|
* @param WC_Subscription $subscription WC Subscription object.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_wcpay_subscription( WC_Subscription $subscription ): bool {
|
|
return ! WC_Payments_Subscriptions::is_duplicate_site() && WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() && (bool) self::get_wcpay_subscription_id( $subscription );
|
|
}
|
|
|
|
/**
|
|
* Formats item data.
|
|
*
|
|
* @param string $currency The item's currency.
|
|
* @param string $wcpay_product_id The item's Stripe product id.
|
|
* @param float $unit_amount The item's unit amount.
|
|
* @param string $interval The item's interval. Optional.
|
|
* @param int $interval_count The item's interval count. Optional.
|
|
*
|
|
* @return array Structured invoice item array.
|
|
*/
|
|
public static function format_item_price_data( string $currency, string $wcpay_product_id, float $unit_amount, string $interval = '', int $interval_count = 0 ): array {
|
|
$data = [
|
|
'currency' => $currency,
|
|
'product' => $wcpay_product_id,
|
|
// We cannot use WC_Payments_Utils::prepare_amount() here because it returns an int but 'unit_amount_decimal' supports multiple decimal places even though it is cents (fractions of a cent).
|
|
'unit_amount_decimal' => round( $unit_amount, wc_get_rounding_precision() ),
|
|
];
|
|
|
|
// Convert the amount to cents if it's not in a zero based currency.
|
|
if ( ! WC_Payments_Utils::is_zero_decimal_currency( strtolower( $currency ) ) ) {
|
|
$data['unit_amount_decimal'] *= 100;
|
|
}
|
|
|
|
if ( $interval && $interval_count ) {
|
|
$data['recurring'] = [
|
|
'interval' => $interval,
|
|
'interval_count' => $interval_count,
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Prepares discount data used to create a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription used to create the subscription on server.
|
|
*
|
|
* @return array WCPay discount item data.
|
|
*/
|
|
public static function get_discount_item_data_for_subscription( WC_Subscription $subscription ): array {
|
|
$data = [];
|
|
|
|
foreach ( $subscription->get_items( 'coupon' ) as $item ) {
|
|
$code = $item->get_code();
|
|
$coupon = new WC_Coupon( $code );
|
|
$duration = in_array( $coupon->get_discount_type(), [ 'recurring_fee', 'recurring_percent' ], true ) ? 'forever' : 'once';
|
|
$discount = $item->get_discount();
|
|
|
|
if ( $discount ) {
|
|
$data[] = [
|
|
'amount_off' => WC_Payments_Utils::prepare_amount( $discount, $subscription->get_currency() ),
|
|
'currency' => $subscription->get_currency(),
|
|
'duration' => $duration,
|
|
// Translators: %s Coupon code.
|
|
'name' => sprintf( __( 'Coupon - %s', 'woocommerce-payments' ), $code ),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Gets a WCPay subscription from a WC subscription object.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription to get from server.
|
|
*
|
|
* @return array|bool WCPay subscription data, otherwise false.
|
|
*/
|
|
public function get_wcpay_subscription( WC_Subscription $subscription ) {
|
|
$wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription );
|
|
|
|
if ( ! $wcpay_subscription_id ) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return $this->payments_api_client->get_subscription( $wcpay_subscription_id );
|
|
} catch ( API_Exception $e ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC order used to create a wcpay subscription on server.
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws Exception Throws an exception to stop checkout processing and display message to customer.
|
|
*/
|
|
public function create_subscription( WC_Subscription $subscription ) {
|
|
/*
|
|
* Bail early if the subscription payment method is not WooPayments.
|
|
* WCPay Subscriptions are not created in the following scenarios:
|
|
*
|
|
* - A different payment gateway was used to purchase the subscription (e.g. PayPal).
|
|
* - The subscription is free (i.e. $0) and payment details were not captured during checkout.
|
|
*/
|
|
if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $subscription->get_payment_method() ) {
|
|
return;
|
|
}
|
|
|
|
$checkout_error_message = __( 'There was a problem creating your subscription. Please try again or contact us for assistance.', 'woocommerce-payments' );
|
|
$wcpay_customer_id = $this->customer_service->get_customer_id_for_order( $subscription );
|
|
|
|
if ( ! $wcpay_customer_id ) {
|
|
Logger::error( 'There was a problem creating the WooPayments subscription. WooPayments customer ID missing.' );
|
|
throw new Exception( $checkout_error_message );
|
|
}
|
|
|
|
try {
|
|
$subscription_data = $this->prepare_wcpay_subscription_data( $wcpay_customer_id, $subscription );
|
|
$this->validate_subscription_data( $subscription_data );
|
|
|
|
$subscription_data['metadata']['subscription_source'] = $this->is_subscriptions_plugin_active() ? 'woo_subscriptions' : 'wcpay_subscriptions';
|
|
|
|
$response = $this->payments_api_client->create_subscription( $subscription_data );
|
|
|
|
$this->set_wcpay_subscription_id( $subscription, $response['id'] );
|
|
$this->set_wcpay_subscription_item_ids( $subscription, $response['items']['data'] );
|
|
|
|
if ( isset( $response['discounts'] ) ) {
|
|
static::set_wcpay_discount_ids( $subscription, $response['discounts'] );
|
|
}
|
|
|
|
if ( ! empty( $response['latest_invoice'] ) ) {
|
|
$this->invoice_service->set_subscription_invoice_id( $subscription, $response['latest_invoice'] );
|
|
}
|
|
} catch ( \Exception $e ) {
|
|
Logger::log( sprintf( 'There was a problem creating the WooPayments subscription. %s', $e->getMessage() ) );
|
|
|
|
if ( $e instanceof Amount_Too_Small_Exception ) {
|
|
throw new Exception(
|
|
sprintf(
|
|
// Translators: The %1 placeholder is a currency formatted price string ($0.50). The %2 and %3 placeholders are opening and closing strong HTML tags.
|
|
__( 'There was a problem creating your subscription. %1$s doesn\'t meet the %2$sminimum recurring amount%3$s this payment method can process.', 'woocommerce-payments' ),
|
|
wc_price( $subscription->get_total() ),
|
|
'<strong>',
|
|
'</strong>'
|
|
)
|
|
);
|
|
} elseif ( $e instanceof Cannot_Combine_Currencies_Exception ) {
|
|
throw new Exception(
|
|
sprintf(
|
|
// Translators: %1$s and %2$s are both currency codes, e.g. `USD` or `EUR`.
|
|
__( 'The subscription couldn\'t be created because it uses a different currency (%1$s) from your existing subscriptions (%2$s). Please ensure all subscriptions use the same currency.', 'woocommerce-payments' ),
|
|
$subscription->get_currency(),
|
|
$e->get_currency()
|
|
)
|
|
);
|
|
}
|
|
|
|
throw new Exception( $checkout_error_message );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Conditionally creates a WCPay subscription when a subscriber
|
|
* updates the subscription payment method from their account page.
|
|
*
|
|
* @param WC_Subscription $subscription An instance of a WC_Subscription object.
|
|
* @param string $new_payment_method The ID of the new payment method.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function maybe_create_subscription_from_update_payment_method( WC_Subscription $subscription, string $new_payment_method ) {
|
|
// Not changing the subscription payment method to WooPayments, bail.
|
|
if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) {
|
|
return;
|
|
}
|
|
|
|
// We already have a WCPay subscription ID, bail.
|
|
if ( (bool) self::get_wcpay_subscription_id( $subscription ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->is_creating_subscription_from_update_payment_method = true;
|
|
|
|
$this->create_subscription( $subscription );
|
|
}
|
|
|
|
/**
|
|
* Cancels the WCPay subscription when it's cancelled in WC.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription that was canceled.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function cancel_subscription( WC_Subscription $subscription ) {
|
|
$wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription );
|
|
|
|
if ( ! $wcpay_subscription_id ) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$this->payments_api_client->cancel_subscription( $wcpay_subscription_id );
|
|
} catch ( API_Exception $e ) {
|
|
Logger::log( sprintf( 'There was a problem canceling the subscription on WooPayments server: %s.', $e->getMessage() ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle subscription status change to on-hold.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function handle_subscription_status_on_hold( WC_Subscription $subscription ) {
|
|
// Check if the subscription is a WCPay subscription before proceeding.
|
|
// In stores that have WC Subscriptions active, or previously had WC S,
|
|
// this method may be called with regular tokenised subscriptions.
|
|
if ( ! static::is_wcpay_subscription( $subscription ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->suspend_subscription( $subscription );
|
|
|
|
// Add an order note as a visible record of suspend.
|
|
$subscription->add_order_note( __( 'Suspended WooPayments Subscription because subscription status changed to on-hold.', 'woocommerce-payments' ) );
|
|
|
|
// Log that the subscription was suspended.
|
|
// Include a brief stack trace to help determine where status change originated.
|
|
// For example, admin user action, or a code interaction with customizations.
|
|
$e = new Exception();
|
|
$trace = $e->getTraceAsString();
|
|
Logger::log(
|
|
sprintf(
|
|
'Suspended WooPayments Subscription because subscription status changed to on-hold. WC ID: %d; WooPayments ID: %s; stack: %s',
|
|
$subscription->get_id(),
|
|
self::get_wcpay_subscription_id( $subscription ),
|
|
$trace
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Suspends a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription to suspend.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function suspend_subscription( WC_Subscription $subscription ) {
|
|
// Check if the subscription is a WCPay subscription before proceeding.
|
|
if ( ! static::is_wcpay_subscription( $subscription ) ) {
|
|
Logger::log(
|
|
sprintf(
|
|
'Aborting WC_Payments_Subscription_Service::suspend_subscription; subscription is a tokenised (non WooPayments) subscription. WC ID: %d.',
|
|
$subscription->get_id()
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
$this->update_subscription( $subscription, [ 'pause_collection' => [ 'behavior' => 'void' ] ] );
|
|
}
|
|
|
|
/**
|
|
* Reactivates the WCPay subscription when the WC subscription is activated.
|
|
* This is done by making a request to server to unset the "cancellation at end of period" value for the WooPayments subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription that was activated.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function reactivate_subscription( WC_Subscription $subscription ) {
|
|
$this->update_subscription(
|
|
$subscription,
|
|
[
|
|
'cancel_at_period_end' => 'false',
|
|
'pause_collection' => '',
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Marks the WCPay subscription as pending-cancel by setting the "cancellation at end of period" on the WooPayments subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The subscription that was set as pending cancel.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function set_pending_cancel_for_subscription( WC_Subscription $subscription ) {
|
|
$this->update_subscription( $subscription, [ 'cancel_at_period_end' => 'true' ] );
|
|
}
|
|
|
|
/**
|
|
* When a WC Subscription's payment method has been updated make sure we attach
|
|
* the new payment method ID to the WCPay subscription.
|
|
*
|
|
* If the WCPay subscription's payment method was updated while there's a failed invoice, trigger a retry.
|
|
*
|
|
* @param int $subscription_id Post ID (WC subscription ID) that had its payment method updated.
|
|
* @param int $token_id Payment Token post ID stored in DB.
|
|
* @param WC_Payment_Token $token Payment Token object.
|
|
*/
|
|
public function update_wcpay_subscription_payment_method( int $subscription_id, int $token_id, WC_Payment_Token $token ) {
|
|
if ( ! function_exists( 'wcs_get_subscription' ) ) {
|
|
return;
|
|
}
|
|
|
|
$subscription = wcs_get_subscription( $subscription_id );
|
|
|
|
if ( $subscription && self::is_wcpay_subscription( $subscription ) ) {
|
|
$wcpay_subscription_id = static::get_wcpay_subscription_id( $subscription );
|
|
$wcpay_payment_method_id = $token->get_token();
|
|
|
|
if ( $wcpay_subscription_id && $wcpay_payment_method_id ) {
|
|
try {
|
|
$this->update_subscription( $subscription, [ 'default_payment_method' => $wcpay_payment_method_id ] );
|
|
} catch ( API_Exception $e ) {
|
|
Logger::error( sprintf( 'There was a problem updating the WooPayments subscription\'s default payment method on server: %s.', $e->getMessage() ) );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts payment for WCPay subscription if needed.
|
|
*
|
|
* @param WC_Subscription $subscription WC subscription linked to the WCPay subscription that maybe needs to retry payment.
|
|
* @param WC_Payment_Token $token The new subscription token to assign to the invoice order.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function maybe_attempt_payment_for_subscription( $subscription, WC_Payment_Token $token ) {
|
|
|
|
if ( ! function_exists( 'wcs_is_subscription' ) || ! wcs_is_subscription( $subscription ) ) {
|
|
return;
|
|
}
|
|
|
|
$wcpay_invoice_id = WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription );
|
|
|
|
if ( ! $wcpay_invoice_id || ! self::is_wcpay_subscription( $subscription ) ) {
|
|
return;
|
|
}
|
|
|
|
$response = $this->payments_api_client->charge_invoice( $wcpay_invoice_id );
|
|
|
|
// Rather than wait for the Stripe webhook to be received, complete the order now if it was successfully paid.
|
|
if ( $response && isset( $response['status'] ) && 'paid' === $response['status'] ) {
|
|
// Remove the pending invoice ID now that we know it has been paid.
|
|
$this->invoice_service->mark_pending_invoice_paid_for_subscription( $subscription );
|
|
|
|
$order_id = WC_Payments_Invoice_Service::get_order_id_by_invoice_id( $wcpay_invoice_id );
|
|
$order = $order_id ? wc_get_order( $order_id ) : false;
|
|
|
|
if ( $order && $order->needs_payment() ) {
|
|
// We're about to record a successful payment, temporarily remove the "is request to change payment method" flag as it prevents us from activating the subscrption via WC_Subscription::payment_complete().
|
|
$is_change_payment_request = WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment;
|
|
WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment = false;
|
|
|
|
// We need to store the successful token on the order otherwise WC_Subscriptions_Change_Payment_Gateway::change_failing_payment_method() will override the successful token with the failing one.
|
|
$order->add_payment_token( $token );
|
|
$order->payment_complete();
|
|
|
|
// Reinstate the "is request to change payment method" flag.
|
|
WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment = $is_change_payment_request;
|
|
wc_add_notice( __( "We've successfully collected payment for your subscription using your new payment method.", 'woocommerce-payments' ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the subscription supports a given feature.
|
|
*
|
|
* @param bool $supported Is feature supported.
|
|
* @param string $feature Feature flag.
|
|
* @param WC_Subscription $subscription WC Subscription to check if feature is supported against.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function prevent_wcpay_subscription_changes( bool $supported, string $feature, WC_Subscription $subscription ) {
|
|
$is_stripe_billing = self::is_wcpay_subscription( $subscription );
|
|
|
|
switch ( $feature ) {
|
|
case 'subscription_amount_changes':
|
|
case 'subscription_date_changes':
|
|
$supported = ! $is_stripe_billing;
|
|
break;
|
|
case 'gateway_scheduled_payments':
|
|
$supported = $is_stripe_billing;
|
|
break;
|
|
}
|
|
|
|
if ( $is_stripe_billing ) {
|
|
$supported = in_array( $feature, $this->supports, true ) || isset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] );
|
|
}
|
|
|
|
return $supported;
|
|
}
|
|
|
|
/**
|
|
* Remove pending parent and renewal order creation from admin edit subscriptions page.
|
|
*
|
|
* @param array $actions Array of available actions.
|
|
* @return array Array of updated actions.
|
|
*/
|
|
public function prevent_wcpay_manual_renewal( array $actions ) {
|
|
global $theorder;
|
|
|
|
if ( ! function_exists( 'wcs_is_subscription' ) || ! $theorder ) {
|
|
return $actions;
|
|
}
|
|
|
|
if ( wcs_is_subscription( $theorder ) && self::is_wcpay_subscription( $theorder ) ) {
|
|
unset(
|
|
$actions['wcs_create_pending_parent'],
|
|
$actions['wcs_create_pending_renewal'],
|
|
$actions['wcs_process_renewal']
|
|
);
|
|
}
|
|
return $actions;
|
|
}
|
|
|
|
/**
|
|
* Show WCPay Subscription ID on Edit Subscription page.
|
|
*
|
|
* @param WC_Order|WC_Subscription $order The order object.
|
|
*/
|
|
public function show_wcpay_subscription_id( WC_Order $order ) {
|
|
if ( ! function_exists( 'wcs_is_subscription' ) || ! wcs_is_subscription( $order ) || ! self::is_wcpay_subscription( $order ) ) {
|
|
return;
|
|
}
|
|
|
|
$wcpay_subscription_id = self::get_wcpay_subscription_id( $order );
|
|
if ( ! $wcpay_subscription_id ) {
|
|
return;
|
|
}
|
|
|
|
echo '<p><strong>' . sprintf(
|
|
/* translators: %s: WooPayments */
|
|
esc_html__( '%s Subscription ID', 'woocommerce-payments' ),
|
|
'WooPayments'
|
|
) . ':</strong> ' . esc_html( $wcpay_subscription_id ) . '</p>';
|
|
}
|
|
|
|
/**
|
|
* Updates a subscription's next payment date to match the WooPayments subscription's payment date.
|
|
*
|
|
* @param array $wcpay_subscription The WCPay Subscription data.
|
|
* @param WC_Subscription $subscription The WC Subscription object.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function update_dates_to_match_wcpay_subscription( array $wcpay_subscription, WC_Subscription $subscription ) {
|
|
// Temporarily allow date changes when we're updating dates to match the dates on the WooPayments subscription.
|
|
$this->set_feature_support_exception( $subscription, 'subscription_date_changes' );
|
|
|
|
$next_payment_date = gmdate( 'Y-m-d H:i:s', $wcpay_subscription['current_period_end'] );
|
|
$subscription->update_dates( [ 'next_payment' => $next_payment_date ] );
|
|
|
|
$next_payment_time_difference = absint( $wcpay_subscription['current_period_end'] - $subscription->get_time( 'next_payment' ) );
|
|
|
|
if ( $next_payment_time_difference > 0 && $next_payment_time_difference >= 12 * HOUR_IN_SECONDS ) {
|
|
$subscription->add_order_note( __( 'The subscription\'s next payment date has been updated to match WooPayments server.', 'woocommerce-payments' ) );
|
|
}
|
|
|
|
// Remove the 'subscription_date_changes' exception.
|
|
$this->clear_feature_support_exception( $subscription, 'subscription_date_changes' );
|
|
}
|
|
|
|
/**
|
|
* Creates a WCPay subscription on successful renewal payment for manual WC subscription.
|
|
*
|
|
* @param int $order_id WC Order ID.
|
|
*/
|
|
public function create_subscription_for_manual_renewal( int $order_id ) {
|
|
if ( ! function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) {
|
|
return;
|
|
}
|
|
|
|
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
|
|
|
|
foreach ( $subscriptions as $subscription_id => $subscription ) {
|
|
if ( ! self::get_wcpay_subscription_id( $subscription ) && $subscription->is_manual() ) {
|
|
$this->create_subscription( $subscription );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares item data used to create a WCPay subscription.
|
|
*
|
|
* @param string $wcpay_customer_id WCPay Customer ID to create the subscription for.
|
|
* @param WC_Subscription $subscription The WC subscription used to create the subscription on server.
|
|
*
|
|
* @return array WCPay subscription data
|
|
*/
|
|
private function prepare_wcpay_subscription_data( string $wcpay_customer_id, WC_Subscription $subscription ) {
|
|
$recurring_items = $this->get_recurring_item_data_for_subscription( $subscription );
|
|
$one_time_items = $this->get_one_time_item_data_for_subscription( $subscription );
|
|
$discount_items = self::get_discount_item_data_for_subscription( $subscription );
|
|
$data = [
|
|
'customer' => $wcpay_customer_id,
|
|
'items' => $recurring_items,
|
|
];
|
|
|
|
if ( self::has_delayed_payment( $subscription ) ) {
|
|
$data['trial_end'] = max( $subscription->get_time( 'trial_end' ), $subscription->get_time( 'next_payment' ) );
|
|
}
|
|
|
|
if ( ! empty( $one_time_items ) ) {
|
|
$data['add_invoice_items'] = $one_time_items;
|
|
}
|
|
|
|
if ( ! empty( $discount_items ) ) {
|
|
$data['discounts'] = $discount_items;
|
|
}
|
|
|
|
if ( $this->is_creating_subscription_from_update_payment_method ) {
|
|
$data['backdate_start_date'] = max( $subscription->get_time( 'start' ), $subscription->get_time( 'last_order_date_created' ), $subscription->get_time( 'last_order_date_paid' ) );
|
|
$data['billing_cycle_anchor'] = $subscription->get_time( 'next_payment' );
|
|
}
|
|
|
|
return apply_filters( 'wcpay_subscriptions_prepare_subscription_data', $data );
|
|
}
|
|
|
|
/**
|
|
* Gets recurring item data from a subscription needed to create a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription to fetch product data from.
|
|
*
|
|
* @return array WCPay recurring item data.
|
|
*/
|
|
public function get_recurring_item_data_for_subscription( WC_Subscription $subscription ): array {
|
|
$data = [];
|
|
|
|
foreach ( $subscription->get_items() as $item ) {
|
|
$data[] = [
|
|
'metadata' => $this->get_item_metadata( $item ),
|
|
'quantity' => $item->get_quantity(),
|
|
'price_data' => static::format_item_price_data( $subscription->get_currency(), $this->product_service->get_or_create_wcpay_product_id( $item->get_product() ), $item->get_subtotal() / $item->get_quantity(), $subscription->get_billing_period(), $subscription->get_billing_interval() ),
|
|
];
|
|
}
|
|
|
|
$additional_items = array_merge( $subscription->get_fees(), $subscription->get_shipping_methods(), $subscription->get_taxes() );
|
|
|
|
foreach ( $additional_items as $item ) {
|
|
if ( is_a( $item, 'WC_Order_Item_Tax' ) ) {
|
|
$item_name = $item->get_label();
|
|
$unit_amount = $item->get_tax_total() + $item->get_shipping_tax_total();
|
|
} else {
|
|
$item_name = $item->get_type();
|
|
$unit_amount = $item->get_total();
|
|
}
|
|
|
|
if ( $unit_amount ) {
|
|
$data[] = [
|
|
'metadata' => $this->get_item_metadata( $item ),
|
|
'price_data' => self::format_item_price_data(
|
|
$subscription->get_currency(),
|
|
$this->product_service->get_wcpay_product_id_for_item( $item_name ),
|
|
$unit_amount,
|
|
$subscription->get_billing_period(),
|
|
$subscription->get_billing_interval()
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Cancels a WCPay subscription when a customer changes their payment method
|
|
*
|
|
* @param WC_Subscription $subscription The subscription that was updated.
|
|
* @param string $new_payment_method The subscription's new payment method ID.
|
|
*/
|
|
public function maybe_cancel_subscription( $subscription, $new_payment_method ) {
|
|
$wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription );
|
|
|
|
if ( (bool) $wcpay_subscription_id && WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) {
|
|
$this->cancel_subscription( $subscription );
|
|
|
|
// Delete the WCPay Subscription meta but keep a record of it.
|
|
$subscription->update_meta_data( '_cancelled' . self::SUBSCRIPTION_ID_META_KEY, $wcpay_subscription_id );
|
|
$subscription->delete_meta_data( self::SUBSCRIPTION_ID_META_KEY );
|
|
$subscription->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the original subscription mode matches current WooPayments mode.
|
|
*
|
|
* If the original subscription was payed with WooPayments, but in the mode, that doesn't
|
|
* match the current WooPayments mode, we need to throw an exception, to prevent the renewal
|
|
* order from being created, as it would fail to be paid.
|
|
*
|
|
* @param array $items The items to be added to the renewal order.
|
|
* @param WC_Order $order Renewal order.
|
|
* @param WC_Subscription $subscription The original subscription.
|
|
* @throws Subscription_Mode_Mismatch_Exception
|
|
* @return array
|
|
*/
|
|
public function check_wcpay_mode_for_subscription( array $items, WC_Order $order, WC_Subscription $subscription ): array {
|
|
$parent_order = $subscription->get_parent();
|
|
if ( false !== $parent_order ) {
|
|
$subscription_mode = $parent_order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY );
|
|
$current_mode = WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION;
|
|
|
|
if ( is_string( $subscription_mode ) && '' !== $subscription_mode && $subscription_mode !== $current_mode ) {
|
|
if ( Order_Mode::TEST === $subscription_mode ) {
|
|
throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the test mode and cannot be renewed in the live mode.', 'woocommerce-payments' ) );
|
|
} else {
|
|
throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the live mode and cannot be renewed in the test mode.', 'woocommerce-payments' ) );
|
|
}
|
|
}
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Gets one time item data from a subscription needed to create a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription to fetch item data from.
|
|
*
|
|
* @return array WCPay one time item data.
|
|
*/
|
|
private function get_one_time_item_data_for_subscription( WC_Subscription $subscription ): array {
|
|
$data = [];
|
|
$currency = $subscription->get_currency();
|
|
|
|
foreach ( $subscription->get_items() as $item ) {
|
|
$product = $item->get_product();
|
|
$sign_up_fee = (float) WC_Subscriptions_Product::get_sign_up_fee( $product );
|
|
$one_time_shipping = WC_Subscriptions_Product::needs_one_time_shipping( $product );
|
|
|
|
if ( $sign_up_fee ) {
|
|
$wcpay_item_id = $this->product_service->get_wcpay_product_id_for_item( 'sign_up_fee' );
|
|
$data[] = [
|
|
'price_data' => self::format_item_price_data( $currency, $wcpay_item_id, $sign_up_fee ),
|
|
];
|
|
}
|
|
|
|
if ( $one_time_shipping ) {
|
|
$wcpay_item_id = $this->product_service->get_wcpay_product_id_for_item( 'shipping' );
|
|
$shipping = 0;
|
|
|
|
foreach ( $subscription->get_parent()->get_shipping_methods() as $shipping_method ) {
|
|
$shipping += $shipping_method->get_total();
|
|
}
|
|
|
|
$data[] = [
|
|
'price_data' => self::format_item_price_data( $currency, $wcpay_item_id, $shipping ),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Updates a WCPay subscription.
|
|
*
|
|
* @param WC_Subscription $subscription The WC subscription that relates to the WCPay subscription that needs updating.
|
|
* @param array $data Data to update.
|
|
*
|
|
* @return array|null Updated wcpay subscription or null if there was an error.
|
|
*/
|
|
private function update_subscription( WC_Subscription $subscription, array $data ) {
|
|
$wcpay_subscription_id = static::get_wcpay_subscription_id( $subscription );
|
|
$response = null;
|
|
|
|
if ( ! $wcpay_subscription_id ) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$response = $this->payments_api_client->update_subscription( $wcpay_subscription_id, $data );
|
|
} catch ( API_Exception $e ) {
|
|
Logger::log( sprintf( 'There was a problem updating the WooPayments subscription on server: %s', $e->getMessage() ) );
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Set the trial end date for the WCPay subscription (this updates both trial end as well as next payment).
|
|
*
|
|
* @param WC_Subscription $subscription WC subscription linked to the WCPay subscription that needs updating.
|
|
* @param int $timestamp Next payment or trial end timestamp in UTC.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function set_trial_end_for_subscription( WC_Subscription $subscription, int $timestamp ) {
|
|
$trial_end = 0 === $timestamp ? 'now' : $timestamp;
|
|
$this->update_subscription( $subscription, [ 'trial_end' => $trial_end ] );
|
|
}
|
|
|
|
/**
|
|
* Sets the WCPay subscription ID meta for WC subscription.
|
|
*
|
|
* @param WC_Subscription $subscription WC Subscription to store meta against.
|
|
* @param string $value WCPay subscription ID meta value.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function set_wcpay_subscription_id( WC_Subscription $subscription, string $value ) {
|
|
$subscription->update_meta_data( self::SUBSCRIPTION_ID_META_KEY, $value );
|
|
$subscription->save();
|
|
}
|
|
|
|
/**
|
|
* Sets Stripe subscription item ids on WC order items.
|
|
*
|
|
* @param WC_Subscription $subscription The WC Subscription object.
|
|
* @param array $subscription_items The WCPay Subscription data.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function set_wcpay_subscription_item_ids( WC_Subscription $subscription, array $subscription_items ) {
|
|
foreach ( $subscription_items as $item ) {
|
|
$wcpay_subscription_item_id = $item['id'];
|
|
$subscription_item_id = isset( $item['metadata']['wc_item_id'] ) ? $item['metadata']['wc_item_id'] : false;
|
|
|
|
if ( $subscription_item_id ) {
|
|
$subscription_item = $subscription->get_item( $subscription_item_id );
|
|
$subscription_item->update_meta_data( self::SUBSCRIPTION_ITEM_ID_META_KEY, $wcpay_subscription_item_id );
|
|
$subscription_item->save();
|
|
} else {
|
|
Logger::log(
|
|
sprintf(
|
|
// Translators: %s Stripe subscription item ID.
|
|
__( 'Unable to set subscription item ID meta for WooPayments subscription item %s.', 'woocommerce-payments' ),
|
|
$wcpay_subscription_item_id
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Temporarily allows a subscription to bypass a payment gateway feature support flag.
|
|
*
|
|
* Use @see WC_Payments_Subscription_Service::clear_feature_support_exception() to clear it.
|
|
*
|
|
* @param WC_Subscription $subscription The subscription to set the exception for.
|
|
* @param string $feature The feature to allow.
|
|
*/
|
|
private function set_feature_support_exception( WC_Subscription $subscription, string $feature ) {
|
|
$this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] = true;
|
|
}
|
|
|
|
/**
|
|
* Clears a gateway support flag exception.
|
|
*
|
|
* Use @see WC_Payments_Subscription_Service::set_feature_support_exception() to set one.
|
|
*
|
|
* @param WC_Subscription $subscription The subscription to remove the exception for.
|
|
* @param string $feature The feature.
|
|
*/
|
|
private function clear_feature_support_exception( WC_Subscription $subscription, string $feature ) {
|
|
unset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] );
|
|
}
|
|
|
|
/**
|
|
* Generates the metadata for a given WC_Order_Item
|
|
*
|
|
* @param WC_Order_Item|WC_Order_Item_Tax $item The order item to generate the metadata for. Can be any order item type including tax, shipping and fees.
|
|
* @return array Item metadata.
|
|
*/
|
|
private function get_item_metadata( WC_Order_Item $item ) {
|
|
$metadata = [ 'wc_item_id' => $item->get_id() ];
|
|
|
|
switch ( $item->get_type() ) {
|
|
case 'tax':
|
|
$metadata['wc_rate_id'] = $item->get_rate_id();
|
|
$metadata['code'] = $item->get_rate_code();
|
|
$metadata['rate'] = $item->get_rate_percent();
|
|
$metadata['is_compound'] = wc_bool_to_string( $item->is_compound() );
|
|
break;
|
|
case 'shipping':
|
|
$metadata['method'] = $item->get_name();
|
|
break;
|
|
case 'fee':
|
|
$metadata['type'] = $item->get_name();
|
|
break;
|
|
}
|
|
|
|
return $metadata;
|
|
}
|
|
|
|
/**
|
|
* Validates that the data used to create the WCPay Subscription.
|
|
*
|
|
* @param array $subscription_data The data used to create a WCPay subscription.
|
|
* @throws Exception If the subscription data contains invalid or missing data.
|
|
*/
|
|
private function validate_subscription_data( $subscription_data ) {
|
|
|
|
if ( empty( $subscription_data['customer'] ) ) {
|
|
throw new Exception( 'The "customer" arg is required to create the subscription.' );
|
|
}
|
|
|
|
if ( ! isset( $subscription_data['items'] ) ) {
|
|
throw new Exception( 'The "items" arg is required to create the subscription.' );
|
|
}
|
|
|
|
foreach ( $subscription_data['items'] as $item_data ) {
|
|
$required_price_keys = [ 'currency', 'product', 'recurring' ];
|
|
$required_period_keys = [ 'interval', 'interval_count' ];
|
|
$errors = [];
|
|
|
|
if ( ! isset( $item_data['price_data']['unit_amount_decimal'] ) ) {
|
|
$errors[] = 'unit_amount_decimal';
|
|
}
|
|
|
|
foreach ( $required_price_keys as $required_key ) {
|
|
if ( empty( $item_data['price_data'][ $required_key ] ) ) {
|
|
$errors[] = $required_key;
|
|
}
|
|
}
|
|
|
|
foreach ( $required_period_keys as $required_price_key ) {
|
|
if ( empty( $item_data['price_data']['recurring'][ $required_price_key ] ) ) {
|
|
$errors[] = $required_price_key;
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $errors ) ) {
|
|
$error_message = count( $errors ) > 1 ? 'The "%s" line item properties are required to create the subscription.' : 'The "%s" line item property is required to create the subscription.';
|
|
throw new Exception( sprintf( $error_message, implode( '", "', $errors ) ) );
|
|
}
|
|
|
|
$billing_period = $item_data['price_data']['recurring']['interval'];
|
|
$billing_interval = $item_data['price_data']['recurring']['interval_count'];
|
|
|
|
// Confirm the billing period is valid (no greater than 1 year in length).
|
|
if ( ! $this->product_service->is_valid_billing_cycle( $billing_period, $billing_interval ) ) {
|
|
throw new Exception( sprintf( 'The subscription billing period cannot be any longer than one year. A billing period of "every %s %s(s)" was given.', $billing_interval, $billing_period ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if the store has any active WCPay subscriptions.
|
|
*
|
|
* @return bool True if store has active WCPay subscriptions, otherwise false.
|
|
*/
|
|
public static function store_has_active_wcpay_subscriptions() {
|
|
if ( ! function_exists( 'wcs_get_subscriptions' ) ) {
|
|
return false;
|
|
}
|
|
|
|
$active_wcpay_subscriptions = wcs_get_subscriptions(
|
|
[
|
|
'subscriptions_per_page' => 1,
|
|
'subscription_status' => 'active',
|
|
// Ignoring phpcs warning, we need to search meta.
|
|
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
[
|
|
'key' => self::SUBSCRIPTION_ID_META_KEY,
|
|
'compare' => 'EXISTS',
|
|
],
|
|
],
|
|
]
|
|
);
|
|
|
|
return ( is_countable( $active_wcpay_subscriptions ) ? count( $active_wcpay_subscriptions ) : 0 ) > 0;
|
|
}
|
|
}
|