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; } }