payments_api_client = $payments_api_client;
$this->account = $account;
/**
* When a store is in staging mode, we don't want any product handling to be sent to the server.
*
* Sending these requests from staging sites can have unintended consequences for the live store. For example,
* deleting a subscription product on a staging site would delete the product record at Stripe and that product
* would be in use for the live site.
*/
if ( WC_Payments_Subscriptions::is_duplicate_site() ) {
return;
}
// Only create, update and restore/unarchive WCPay Subscription products when Stripe Billing is active.
if ( WC_Payments_Features::should_use_stripe_billing() ) {
add_action( 'shutdown', [ $this, 'create_or_update_products' ] );
add_action( 'untrashed_post', [ $this, 'maybe_unarchive_product' ] );
add_action( 'wp_trash_post', [ $this, 'maybe_archive_product' ] );
$this->add_product_update_listeners();
}
add_filter( 'woocommerce_duplicate_product_exclude_meta', [ $this, 'exclude_meta_wcpay_product' ] );
}
/**
* Gets the WC Pay product hash associated with a WC product.
*
* @param WC_Product $product The product to get the hash for.
* @return string The product's hash or an empty string.
*/
public static function get_wcpay_product_hash( WC_Product $product ): string {
return $product->get_meta( self::PRODUCT_HASH_KEY, true );
}
/**
* Get or create the WC Pay product ID associated with a WC product.
*
* @param WC_Product $product The product to get the WC Pay ID for.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return string The WC Pay product ID or an empty string.
* @throws Exception
*/
public function get_or_create_wcpay_product_id( WC_Product $product, $test_mode = null ): string {
// If the subscription product doesn't have a WC Pay product ID, create one.
if ( ! $this->has_wcpay_product_id( $product, $test_mode ) ) {
$is_current_environment = null === $test_mode || WC_Payments::mode()->is_test() === $test_mode;
// Only create a new wcpay product if we're trying to fetch a wcpay product ID in the current environment.
if ( $is_current_environment ) {
$this->create_product( $product );
}
}
return $product->get_meta( self::get_wcpay_product_id_meta_key( $test_mode ), true );
}
/**
* Get the WCPay product ID for an item type.
*
* @param string $type The item type.
*
* @return string The WCPay product ID.
* @throws API_Exception
*/
public function get_wcpay_product_id_for_item( string $type ): string {
$sanitized_type = self::sanitize_option_key( $type );
$option_key_name = self::get_wcpay_product_id_option( $sanitized_type );
$wcpay_product_id = get_option( $option_key_name );
// Case 1: No product found, create a new one.
if ( ! $wcpay_product_id ) {
return $this->create_product_for_item_type( $sanitized_type );
}
// For existing products, check the linked account.
$linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type );
$linked_account_id = get_option( $linked_option_key );
$stripe_account_id = $this->account->get_stripe_account_id();
// Case 2: Product exists but linked account doesn't, validate and update if needed.
if ( ! $linked_account_id ) {
try {
// Validate that the product exists for the current account.
$existing_product = $this->payments_api_client->get_product_by_id( $wcpay_product_id );
if ( $existing_product ) {
// Product exists, save with current account ID.
$this->save_wcpay_product_data( $wcpay_product_id, $stripe_account_id, $sanitized_type );
return $wcpay_product_id;
} else {
// Product doesn't exist, create new one.
return $this->create_product_for_item_type( $sanitized_type );
}
} catch ( \Exception $e ) {
// Error occurred, create new product.
Logger::log(
sprintf(
'Error occurred when fetching product : wcpay_product_id=%s, account_id=%s, error=%s',
$wcpay_product_id,
$stripe_account_id,
$e->getMessage()
)
);
return $this->create_product_for_item_type( $sanitized_type );
}
}
// Case 3: Product exists but for a different Stripe account, create new one.
if ( $linked_account_id !== $stripe_account_id ) {
return $this->create_product_for_item_type( $sanitized_type );
}
// Case 4: Valid product exists for current account.
return $wcpay_product_id;
}
/**
* Sanitize option key string to replace space with underscore, and remove special characters.
*
* @param string $type Non sanitized input.
* @return string Sanitized output.
*/
public static function sanitize_option_key( string $type ) {
return sanitize_key( str_replace( ' ', '_', trim( $type ) ) );
}
/**
* Save wcpay product data across two related options.
*
* @param string $wcpay_product_id The WooCommerce Payments product ID.
* @param string $stripe_account_id The Stripe account identifier.
* @param string $type The item type used to construct the option key.
*
* @return void
*/
private function save_wcpay_product_data( string $wcpay_product_id, string $stripe_account_id, string $type ) {
$sanitized_type = self::sanitize_option_key( $type );
$option_key_name = self::get_wcpay_product_id_option( $sanitized_type );
// Store product ID.
update_option( $option_key_name, $wcpay_product_id );
// Store linked stripe account ID.
$linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type );
update_option( $linked_option_key, $stripe_account_id );
}
/**
* Check if the WC product has a valid WC Pay product ID linked to the current Stripe account.
*
* @param WC_Product $product The product to get the WC Pay ID for.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return bool Whether the product has a valid WCPay product ID.
*/
public function has_wcpay_product_id( WC_Product $product, $test_mode = null ): bool {
$option_key = self::get_wcpay_product_id_meta_key( $test_mode );
$wcpay_product_id = $product->get_meta( $option_key );
// No product ID exists.
if ( empty( $wcpay_product_id ) ) {
return false;
}
// Check if we have the linked account metadata.
$linked_option_key = self::get_wcpay_product_id_linked_to_key( null, $test_mode );
$linked_account_id = $product->get_meta( $linked_option_key );
$current_account_id = $this->account->get_stripe_account_id();
// If we have linked account metadata, just compare with current account.
if ( ! empty( $linked_account_id ) ) {
return $linked_account_id === $current_account_id;
}
// Legacy case: we have a product ID but no linked account.
// Verify if product exists for current account.
try {
$product_data = $this->payments_api_client->get_product_by_id( $wcpay_product_id );
// Product exists, update metadata with current account.
if ( ! empty( $product_data ) ) {
$product->update_meta_data( $linked_option_key, $current_account_id );
$product->save();
return true;
}
return false;
} catch ( \Exception $e ) {
Logger::log(
sprintf(
'Error validating WooPayments product: product_id=%d, wcpay_product_id=%s, account_id=%s, error=%s',
$product->get_id(),
$wcpay_product_id,
$current_account_id,
$e->getMessage()
)
);
return false;
}
}
/**
* Prevents duplicate WC Pay product IDs and hashes when duplicating a subscription product.
*
* @param array $meta_keys The keys to exclude from the duplicate.
* @return array Keys to exclude.
*/
public static function exclude_meta_wcpay_product( $meta_keys ) {
return array_merge(
$meta_keys,
[
self::PRODUCT_HASH_KEY,
self::LIVE_PRODUCT_ID_KEY,
self::TEST_PRODUCT_ID_KEY,
self::PRICE_HASH_KEY,
self::LIVE_PRICE_ID_KEY,
self::TEST_PRICE_ID_KEY,
]
);
}
/**
* Schedules a subscription product to be created or updated in WC Pay on shutdown.
*
* @since 3.2.0
*
* @param int $product_id The ID of the product to handle.
*/
public function maybe_schedule_product_create_or_update( int $product_id ) {
if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
return;
}
// Skip products which have already been scheduled or aren't subscriptions.
$product = wc_get_product( $product_id );
if ( ! $product || isset( $this->products_to_update[ $product_id ] ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) {
return;
}
foreach ( $this->get_products_to_update( $product ) as $product_to_update ) {
// Skip products already scheduled.
if ( isset( $this->products_to_update[ $product_to_update->get_id() ] ) ) {
continue;
}
// Skip product variations that don't have a price set.
if ( $product_to_update->is_type( 'subscription_variation' ) && '' === $product_to_update->get_price() ) {
continue;
}
if ( ! $this->has_wcpay_product_id( $product_to_update ) || $this->product_needs_update( $product_to_update ) ) {
$this->products_to_update[ $product_to_update->get_id() ] = $product_to_update->get_id();
}
}
}
/**
* Creates and updates all products which have been scheduled for an update.
*
* Hooked onto shutdown so all products which have been changed in the current request can be updated once.
*
* @since 3.2.0
*/
public function create_or_update_products() {
foreach ( $this->products_to_update as $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
continue;
}
$this->update_products( $product );
}
}
/**
* Creates a product in WC Pay.
*
* @param WC_Product $product The product to create.
*/
public function create_product( WC_Product $product ) {
try {
$product_data = $this->get_product_data( $product );
$stripe_account_id = $this->account->get_stripe_account_id();
// Validate that we have enough data to create the product.
$this->validate_product_data( $product_data );
$wcpay_product = $this->payments_api_client->create_product( $product_data );
$this->remove_product_update_listeners();
$this->set_wcpay_product_hash( $product, $this->get_product_hash( $product ) );
$this->set_wcpay_product_id( $product, $wcpay_product['wcpay_product_id'], $stripe_account_id );
$this->add_product_update_listeners();
} catch ( \Exception $e ) {
Logger::log( sprintf( 'There was a problem creating the product #%s in WC Pay: %s', $product->get_id(), $e->getMessage() ) );
}
}
/**
* Create a generic item product in WC Pay.
*
* @param string $type The item type to create a product for.
*
* @return string The created WCPay product ID.
* @throws API_Exception
*/
private function create_product_for_item_type( string $type ): string {
$wcpay_product = $this->payments_api_client->create_product(
[
'description' => 'N/A',
'name' => ucfirst( $type ),
]
);
$stripe_account_id = $this->account->get_stripe_account_id();
$this->save_wcpay_product_data( $wcpay_product['wcpay_product_id'], $stripe_account_id, $type );
return $wcpay_product['wcpay_product_id'];
}
/**
* Updates related products in WC Pay when a WC Product is updated.
*
* @param WC_Product $product The product to update.
*/
public function update_products( WC_Product $product ) {
if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) {
return;
}
$wcpay_product_ids = $this->get_all_wcpay_product_ids( $product );
$test_mode = WC_Payments::mode()->is_test();
// If the current environment doesn't have a product ID, make sure we create one.
if ( ! isset( $wcpay_product_ids[ $test_mode ? 'test' : 'live' ] ) ) {
$this->create_product( $product );
}
// Return when there's no products to update.
if ( empty( $wcpay_product_ids ) ) {
return;
}
if ( ! $this->product_needs_update( $product ) ) {
return;
}
$data = $this->get_product_data( $product );
$this->remove_product_update_listeners();
try {
// Validate that we have enough data to create the product.
$this->validate_product_data( $data );
// Update all versions of WCPay Products that need updating.
foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) {
$data['test_mode'] = 'live' !== $environment;
$this->payments_api_client->update_product( $wcpay_product_id, $data );
}
$this->set_wcpay_product_hash( $product, $this->get_product_hash( $product ) );
} catch ( \Exception $e ) {
Logger::log( sprintf( 'There was a problem updating the product #%s in WC Pay: %s', $product->get_id(), $e->getMessage() ) );
}
$this->add_product_update_listeners();
}
/**
* Archives a subscription product in WC Pay.
*
* @since 3.2.0
*
* @param int $post_id The ID of the post to handle. Only subscription product IDs will be archived in WC Pay.
*/
public function maybe_archive_product( int $post_id ) {
if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
return;
}
$product = wc_get_product( $post_id );
if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) {
foreach ( $this->get_products_to_update( $product ) as $product ) {
$this->archive_product( $product );
}
}
}
/**
* Unarchives a subscription product in WC Pay.
*
* @since 3.2.0
*
* @param int $post_id The ID of the post to handle. Only Subscription product post IDs will be unarchived in WC Pay.
*/
public function maybe_unarchive_product( int $post_id ) {
if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
return;
}
$product = wc_get_product( $post_id );
if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) {
foreach ( $this->get_products_to_update( $product ) as $product ) {
$this->unarchive_product( $product );
}
}
}
/**
* Archives all related WCPay products (live and test) when a product is trashed/deleted in WC.
*
* @param WC_Product $product The product to archive.
*/
public function archive_product( WC_Product $product ) {
$wcpay_product_ids = $this->get_all_wcpay_product_ids( $product );
if ( empty( $wcpay_product_ids ) ) {
return;
}
foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) {
try {
$this->delete_all_wcpay_price_ids( $product );
$this->payments_api_client->update_product(
$wcpay_product_id,
[
'active' => 'false',
'test_mode' => 'live' !== $environment,
]
);
} catch ( API_Exception $e ) {
Logger::log( 'There was a problem archiving the ' . $environment . ' product in WC Pay: ' . $e->getMessage() );
}
}
}
/**
* Unarchives all related WCPay products (live and test) when a product in WC is untrashed.
*
* @param WC_Product $product The product unarchive.
*/
public function unarchive_product( WC_Product $product ) {
$wcpay_product_ids = $this->get_all_wcpay_product_ids( $product );
if ( empty( $wcpay_product_ids ) ) {
return;
}
foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) {
try {
$this->payments_api_client->update_product(
$wcpay_product_id,
[
'active' => 'true',
'test_mode' => 'live' !== $environment,
]
);
} catch ( API_Exception $e ) {
Logger::log( 'There was a problem unarchiving the ' . $environment . 'product in WC Pay: ' . $e->getMessage() );
}
}
}
/**
* Archives a WC Pay price object.
*
* @param string $wcpay_price_id The price object's ID to archive.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*/
public function archive_price( string $wcpay_price_id, $test_mode = null ) {
$data = [ 'active' => 'false' ];
if ( null !== $test_mode ) {
$data['test_mode'] = $test_mode;
}
$this->payments_api_client->update_price( $wcpay_price_id, $data );
}
/**
* Prevents the subscription interval to be greater than 1 for yearly subscriptions.
*
* @param int $product_id ID of the product that's being saved.
*/
public function limit_subscription_product_intervals( $product_id ) {
if ( $this->is_subscriptions_plugin_active() || ! class_exists( 'WC_Subscriptions_Product' ) ) {
return;
}
// Skip products that aren't subscriptions.
$product = wc_get_product( $product_id );
if (
! $product ||
! WC_Subscriptions_Product::is_subscription( $product ) ||
empty( $_POST['_wcsnonce'] ) ||
! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_subscription_meta' )
) {
return;
}
// If we don't have both the period and the interval, there's nothing to do here.
if ( empty( $_REQUEST['_subscription_period'] ) || empty( $_REQUEST['_subscription_period_interval'] ) ) {
return;
}
$period = sanitize_text_field( wp_unslash( $_REQUEST['_subscription_period'] ) );
$interval = absint( wp_unslash( $_REQUEST['_subscription_period_interval'] ) );
// Prevent WC Subs Core from saving the interval when it's invalid.
if ( ! $this->is_valid_billing_cycle( $period, $interval ) ) {
$new_interval = $this->get_period_interval_limit( $period );
$_REQUEST['_subscription_period_interval'] = (string) $new_interval;
/* translators: %1$s Opening strong tag, %2$s Closing strong tag, %3$s The subscription renewal interval (every x time) */
wcs_add_admin_notice( sprintf( __( '%1$sThere was an issue saving your product!%2$s A subscription product\'s billing period cannot be longer than one year. We have updated this product to renew every %3$s.', 'woocommerce-payments' ), '', '', wcs_get_subscription_period_strings( $new_interval, $period ) ), 'error' );
}
}
/**
* Prevents the subscription interval to be greater than 1 for yearly subscription variations.
*
* @param int $product_id Post ID of the variation.
* @param int $index Variation index in the incoming array.
*/
public function limit_subscription_variation_intervals( $product_id, $index ) {
if ( $this->is_subscriptions_plugin_active() || ! class_exists( 'WC_Subscriptions_Product' ) ) {
return;
}
// Skip products that aren't subscriptions.
$product = wc_get_product( $product_id );
$admin_notice_sent = false;
if (
! $product ||
! WC_Subscriptions_Product::is_subscription( $product ) ||
empty( $_POST['_wcsnonce_save_variations'] ) ||
! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce_save_variations'] ), 'wcs_subscription_variations' )
) {
return;
}
// If we don't have both the period and the interval, there's nothing to do here.
if ( empty( $_POST['variable_subscription_period'][ $index ] ) || empty( $_POST['variable_subscription_period_interval'][ $index ] ) ) {
return;
}
$period = sanitize_text_field( wp_unslash( $_POST['variable_subscription_period'][ $index ] ) );
$interval = absint( wp_unslash( $_POST['variable_subscription_period_interval'][ $index ] ) );
// Prevent WC Subs Core from saving the interval when it's invalid.
if ( ! $this->is_valid_billing_cycle( $period, $interval ) ) {
$new_interval = $this->get_period_interval_limit( $period );
$_POST['variable_subscription_period_interval'][ $index ] = (string) $new_interval;
if ( false === $admin_notice_sent ) {
$admin_notice_sent = true;
/* translators: %1$s Opening strong tag, %2$s Closing strong tag */
wcs_add_admin_notice( sprintf( __( '%1$sThere was an issue saving your variations!%2$s A subscription product\'s billing period cannot be longer than one year. We have updated one or more of this product\'s variations to renew every %3$s.', 'woocommerce-payments' ), '', '', wcs_get_subscription_period_strings( $new_interval, $period ) ), 'error' );
}
}
}
/**
* Attaches the callbacks used to update product changes in WC Pay.
*/
private function add_product_update_listeners() {
// This needs to run before WC_Subscriptions_Admin::save_subscription_meta(), which has a priority of 11.
add_action( 'save_post', [ $this, 'limit_subscription_product_intervals' ], 10 );
// This needs to run before WC_Subscriptions_Admin::save_product_variation(), which has a priority of 20.
add_action( 'woocommerce_save_product_variation', [ $this, 'limit_subscription_variation_intervals' ], 19, 2 );
add_action( 'save_post_product', [ $this, 'maybe_schedule_product_create_or_update' ], 12 );
add_action( 'woocommerce_save_product_variation', [ $this, 'maybe_schedule_product_create_or_update' ], 30 );
}
/**
* Removes the callbacks used to update product changes in WC Pay.
*/
private function remove_product_update_listeners() {
remove_action( 'save_post', [ $this, 'limit_subscription_product_intervals' ], 10 );
remove_action( 'woocommerce_save_product_variation', [ $this, 'limit_subscription_variation_intervals' ], 19 );
remove_action( 'save_post_product', [ $this, 'maybe_schedule_product_create_or_update' ], 12 );
remove_action( 'woocommerce_save_product_variation', [ $this, 'maybe_schedule_product_create_or_update' ], 30 );
}
/**
* Gets product data relevant to WC Pay from a WC product.
*
* @param WC_Product $product The product to get data from.
* @return array
*/
private function get_product_data( WC_Product $product ): array {
return [
'description' => $product->get_description() ? $product->get_description() : 'N/A',
'name' => $product->get_name(),
];
}
/**
* Gets the products to update from a given product.
*
* If applicable, returns the product's variations otherwise returns the product by itself.
*
* @param WC_Product|WC_Product_Variable $product The product.
*
* @return array The products to update.
*/
private function get_products_to_update( WC_Product $product ): array {
return $product->is_type( 'variable-subscription' ) ? $product->get_available_variations( 'object' ) : [ $product ];
}
/**
* Gets a hash of the product's name and description.
* Used to compare WC changes with WC Pay data.
*
* @param WC_Product $product The product to generate the hash for.
* @return string The product's hash.
*/
private function get_product_hash( WC_Product $product ): string {
return md5( implode( $this->get_product_data( $product ) ) );
}
/**
* Checks if a product needs to be updated in WC Pay.
*
* @param WC_Product $product The product to check updates for.
*
* @return bool Whether the product needs to be update in WC Pay.
*/
private function product_needs_update( WC_Product $product ): bool {
return $this->get_product_hash( $product ) !== static::get_wcpay_product_hash( $product );
}
/**
* Sets a WC Pay product hash on a WC product.
*
* @param WC_Product $product The product to set the WC Pay product hash for.
* @param string $value The WC Pay product hash.
*/
private function set_wcpay_product_hash( WC_Product $product, string $value ) {
$product->update_meta_data( self::PRODUCT_HASH_KEY, $value );
$product->save();
}
/**
* Sets a WC Pay product ID and the Stripe account it's linked to on a WC product.
*
* @param WC_Product $product The product to set the WC Pay ID for.
* @param string $wcpay_product_id The WC Pay product ID.
* @param string $stripe_account_id The Stripe account ID.
*/
private function set_wcpay_product_id( WC_Product $product, string $wcpay_product_id, string $stripe_account_id ) {
$option_key = self::get_wcpay_product_id_meta_key();
$link_key = self::get_wcpay_product_id_linked_to_key();
$product->update_meta_data( $option_key, $wcpay_product_id );
$product->update_meta_data( $link_key, $stripe_account_id );
$product->save();
}
/**
* Returns the name of the product id option meta, taking test mode into account.
*
* @param string|null $type The item type.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return string The WCPay product ID meta key/option name.
*/
public static function get_wcpay_product_id_option( ?string $type = null, ?bool $test_mode = null ): string {
$test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode;
$key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY;
return $type ? $key . '_' . $type : $key;
}
/**
* Returns the name of the product id linked to account option meta, taking test mode into account.
*
* @param string|null $type The item type.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return string The WCPay product ID meta key/option name.
*/
public static function get_wcpay_product_id_linked_to_key( ?string $type = null, ?bool $test_mode = null ): string {
$test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode;
$key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY;
return ( $type ? $key . '_' . $type : $key ) . '_linked_to';
}
/**
* Returns the name of the wcpay product id meta key.
*
* @param bool|null $test_mode Is WCPay in test, prod or dev mode.
*
* @return string The product id meta key.
* @throws Exception
*/
public static function get_wcpay_product_id_meta_key( $test_mode = null ): string {
// This functions looks the same as the one above.
// It's here to avoid potential issue when we change the above function.
$test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode;
return $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY;
}
/**
* Returns the name of the price id option meta, taking test mode into account.
*
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return string The price hash option name.
*/
public static function get_wcpay_price_id_option( $test_mode = null ): string {
$test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode;
return $test_mode ? self::TEST_PRICE_ID_KEY : self::LIVE_PRICE_ID_KEY;
}
/**
* Gets all WCPay Product IDs linked to a WC Product (live and testmode products).
*
* @param WC_Product $product The product to fetch WCPay product IDs for.
*
* @return array Live and test WCPay Product IDs if they exist.
*/
private function get_all_wcpay_product_ids( WC_Product $product ) {
$environment_product_ids = [
'live' => $this->has_wcpay_product_id( $product, false ) ? $this->get_or_create_wcpay_product_id( $product, false ) : null,
'test' => $this->has_wcpay_product_id( $product, true ) ? $this->get_or_create_wcpay_product_id( $product, true ) : null,
];
return array_filter( $environment_product_ids );
}
/**
* Returns whether the billing cycle is valid, given its period and interval.
*
* @param string $period Cycle period.
* @param int $interval Cycle interval.
* @return boolean
*/
public function is_valid_billing_cycle( $period, $interval ) {
$interval_limit = $this->get_period_interval_limit( $period );
// A cycle is valid when we have a defined limit, and the given interval isn't 0 nor greater than the limit.
return $interval_limit && ! empty( $interval ) && $interval <= $interval_limit;
}
/**
* Returns the interval limit for the given period.
*
* @param string $period The period to get the interval limit for.
* @return int|bool The interval limit for the period, or false if not defined.
*/
private function get_period_interval_limit( $period ) {
$max_intervals = [
'year' => 1,
'month' => 12,
'week' => 52,
'day' => 365,
];
return ! empty( $max_intervals[ $period ] ) ? $max_intervals[ $period ] : false;
}
/**
* Deletes and archives a product WCPay Price IDs.
*
* @param WC_Product $product The WC Product object to delete and archive the a price IDs.
*/
private function delete_all_wcpay_price_ids( $product ) {
// Delete and archive all price IDs for all environments.
foreach ( [ 'test', 'live' ] as $environment ) {
$test_mode = 'test' === $environment;
$price_id_meta_key = self::get_wcpay_price_id_option( $test_mode );
if ( $product->meta_exists( $price_id_meta_key ) ) {
try {
$this->archive_price( $product->get_meta( $price_id_meta_key, true ), $test_mode );
} catch ( API_Exception $e ) {
Logger::log( 'There was a problem archiving the ' . $environment . 'product price ID in WC Pay: ' . $e->getMessage() );
}
// Now that the price has been archived, delete the record of it.
$product->delete_meta_data( $price_id_meta_key );
$product->delete_meta_data( $price_id_meta_key . '_linked_to' );
}
}
$product->delete_meta_data( self::PRICE_HASH_KEY );
$product->save();
}
/**
* Validates that we have the data necessary to create a product in WCPay.
*
* @param array $product_data Data used to create/update the product in WCPay.
* @throws Exception If the product data doesn't contain the 'name' argument as the 'name' property is a required field.
*/
private function validate_product_data( $product_data ) {
if ( empty( $product_data['name'] ) ) {
throw new Exception( 'The product "name" is required.' );
}
}
/**
* Deprecated functions
*/
/**
* Unarchives a WC Pay Price object.
*
* @deprecated 3.3.0
*
* @param string $wcpay_price_id The Price object's ID to unarchive.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*/
public function unarchive_price( string $wcpay_price_id, $test_mode = null ) {
wc_deprecated_function( __FUNCTION__, '3.3.0' );
$data = [ 'active' => 'true' ];
if ( null !== $test_mode ) {
$data['test_mode'] = $test_mode;
}
$this->payments_api_client->update_price( $wcpay_price_id, $data );
}
/**
* Gets the WC Pay price hash associated with a WC product.
*
* @deprecated 3.3.0
*
* @param WC_Product $product The product to get the hash for.
* @return string The product's price hash or an empty string.
*/
public static function get_wcpay_price_hash( WC_Product $product ): string {
wc_deprecated_function( __FUNCTION__, '3.3.0' );
return $product->get_meta( self::PRICE_HASH_KEY, true );
}
/**
* Gets the WC Pay price ID associated with a WC product.
*
* @deprecated 3.3.0
*
* @param WC_Product $product The product to get the WC Pay price ID for.
* @param bool|null $test_mode Is WC Pay in test/dev mode.
*
* @return string The product's WC Pay price ID or an empty string.
*/
public function get_wcpay_price_id( WC_Product $product, $test_mode = null ): string {
wc_deprecated_function( __FUNCTION__, '3.3.0' );
$price_id = $product->get_meta( self::get_wcpay_price_id_option( $test_mode ), true );
if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
return $price_id;
}
// If the subscription product doesn't have a WC Pay price ID, create one now.
if ( empty( $price_id ) && WC_Subscriptions_Product::is_subscription( $product ) ) {
$is_current_environment = null === $test_mode || WC_Payments::mode()->is_test() === $test_mode;
// Only create WCPay Price object if we're trying to getch a wcpay price ID in the current environment.
if ( $is_current_environment ) {
$this->create_product( $product );
$price_id = $product->get_meta( self::get_wcpay_price_id_option(), true );
}
}
return $price_id;
}
}