diff --git a/includes/class-donations.php b/includes/class-donations.php index fdbeba7237..dbbe011541 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -241,7 +241,7 @@ private static function get_parent_donation_product() { /** * Get the child products of the main donation product. */ - private static function get_donation_product_child_products_ids() { + public static function get_donation_product_child_products_ids() { $child_products_ids = [ 'once' => false, 'month' => false, diff --git a/includes/plugins/woocommerce/class-woocommerce-connection.php b/includes/plugins/woocommerce/class-woocommerce-connection.php index 875acd241a..53a628280b 100644 --- a/includes/plugins/woocommerce/class-woocommerce-connection.php +++ b/includes/plugins/woocommerce/class-woocommerce-connection.php @@ -36,6 +36,7 @@ public static function init() { include_once __DIR__ . '/class-woocommerce-cover-fees.php'; include_once __DIR__ . '/class-woocommerce-order-utm.php'; include_once __DIR__ . '/class-woocommerce-products.php'; + include_once __DIR__ . '/class-woocommerce-product-validator.php'; include_once __DIR__ . '/class-woocommerce-duplicate-orders.php'; include_once __DIR__ . '/class-woocommerce-update-payment-notice.php'; include_once __DIR__ . '/class-woocommerce-custom-currency-symbol.php'; diff --git a/includes/plugins/woocommerce/class-woocommerce-product-validator.php b/includes/plugins/woocommerce/class-woocommerce-product-validator.php new file mode 100644 index 0000000000..3e6d134a4e --- /dev/null +++ b/includes/plugins/woocommerce/class-woocommerce-product-validator.php @@ -0,0 +1,151 @@ + $product->get_id(), + 'product_name' => $product->get_name(), + 'issues' => $issues, + ]; + } + + /** + * Validate basic WooCommerce conditions. + * + * @param \WC_Product $product Product object. + * + * @return array Issues and warnings. + */ + private static function validate_woocommerce_conditions( $product ) { + $issues = []; + + // Check if product is published. + if ( 'publish' !== $product->get_status() ) { + $issues[] = sprintf( + /* translators: %s: product status */ + __( 'Product is not published (status: %s).', 'newspack-plugin' ), + $product->get_status() + ); + } + + // Check if product has a price. + $price = $product->get_price(); + if ( '' === $price || null === $price ) { + $issues[] = __( 'Product has no price set.', 'newspack-plugin' ); + } + + // Check stock status. + if ( ! $product->is_in_stock() ) { + $issues[] = __( 'Product is out of stock.', 'newspack-plugin' ); + } + + // Check if product is purchasable. + if ( ! $product->is_purchasable() ) { + $issues[] = __( 'Product is not purchasable (WooCommerce setting).', 'newspack-plugin' ); + } + + return $issues; + } + + /** + * Validate WooCommerce Memberships restrictions. + * + * @param \WC_Product $product Product object. + * + * @return array Issues. + */ + private static function validate_membership_restrictions( $product ) { + $issues = []; + $product_id = $product->get_id(); + $parent_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product_id; + + // Check viewing restrictions. + if ( function_exists( 'wc_memberships_is_product_viewing_restricted' ) ) { + $is_viewing_restricted = wc_memberships_is_product_viewing_restricted( $product_id ); + if ( ! $is_viewing_restricted && $parent_id !== $product_id ) { + $is_viewing_restricted = wc_memberships_is_product_viewing_restricted( $parent_id ); + } + + if ( $is_viewing_restricted ) { + $issues[] = __( 'Product is restricted by membership rules.', 'newspack-plugin' ); + } + } + + // Check purchasing restrictions. + if ( function_exists( 'wc_memberships_is_product_purchasing_restricted' ) ) { + $is_purchasing_restricted = wc_memberships_is_product_purchasing_restricted( $product_id ); + if ( ! $is_purchasing_restricted && $parent_id !== $product_id ) { + $is_purchasing_restricted = wc_memberships_is_product_purchasing_restricted( $parent_id ); + } + + if ( $is_purchasing_restricted ) { + $issues[] = __( 'Product purchasing is restricted by membership rules.', 'newspack-plugin' ); + + // Get the membership plans that restrict this product. + if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_rules_instance' ) ) { + $rules = wc_memberships()->get_rules_instance()->get_product_restriction_rules( $parent_id ); + $plan_names = []; + + foreach ( $rules as $rule ) { + if ( 'purchase' === $rule->get_access_type() ) { + $plan = wc_memberships_get_membership_plan( $rule->get_membership_plan_id() ); + if ( $plan ) { + $plan_names[] = $plan->get_name(); + } + } + } + + if ( ! empty( $plan_names ) ) { + $issues[] = sprintf( + /* translators: %s: list of membership plan names */ + __( 'Restricted by membership plans: %s', 'newspack-plugin' ), + implode( ', ', array_unique( $plan_names ) ) + ); + } + } + } + } + + return $issues; + } +} diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 9c6efb6f3e..9ee9f78c6e 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -258,6 +258,7 @@ public function fetch_all_data() { ], 'donation_data' => Donations::get_donation_settings(), 'donation_page' => Donations::get_donation_page_info(), + 'product_validation' => $this->validate_donation_products(), ]; if ( 'wc' === $platform ) { $plugin_status = true; @@ -359,4 +360,50 @@ protected function check_required_plugins_installed() { return true; } + + /** + * Validate donation products for purchasability and restrictions. + * + * @return array Validation results for donation products. + */ + protected function validate_donation_products() { + $validation_results = []; + + // Check if WooCommerce is active. + if ( ! class_exists( '\Newspack\WooCommerce_Product_Validator' ) ) { + return $validation_results; + } + + $donation_product_ids = Donations::get_donation_product_child_products_ids( null ); + // Check if we have donation products configured. + if ( empty( array_filter( $donation_product_ids ) ) ) { + return $validation_results; + } + + // Validate each donation product. + foreach ( $donation_product_ids as $frequency => $product_id ) { + if ( empty( $product_id ) ) { + continue; + } + + $validation = WooCommerce_Product_Validator::validate_product_purchasability( $product_id ); + + if ( is_wp_error( $validation ) ) { + $validation_results[ $frequency ] = [ + 'product_id' => $product_id, + 'frequency' => $frequency, + 'issues' => [ $validation->get_error_message() ], + ]; + } else { + $validation_results[ $frequency ] = [ + 'product_id' => $product_id, + 'product_name' => $validation['product_name'], + 'frequency' => $frequency, + 'issues' => $validation['issues'], + ]; + } + } + + return $validation_results; + } } diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index 8ceae6e38c..184d354c34 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -3,7 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; -import { ToggleControl } from '@wordpress/components'; +import { ToggleControl, ExternalLink } from '@wordpress/components'; /** * Internal dependencies. @@ -253,8 +253,50 @@ const Donation = () => { auxData: { saveDonationProduct: true }, } ); + // Check for product validation errors. + const validationResults = Object.values( wizardData.product_validation || {} ); + const hasInvalidProducts = validationResults.some( product => product.issues.length > 0 ); + return ( + { /* Display product validation issues */ } + { hasInvalidProducts ? ( + + + + ) : null } + { wizardData.donation_page && ( <> diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index 59586661dc..c31e796dbb 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -70,6 +70,13 @@ type AudienceFieldConfig = { type: string; validate: string[]; }; + +type ProductValidation = { + product_id: number; + product_name?: string; + frequency: string; + issues: string[]; +}; type AudienceDonationsWizardData = { donation_data: | { errors: { [ key: string ]: string[] } } @@ -97,4 +104,7 @@ type AudienceDonationsWizardData = { [ key: string ]: AudienceFieldConfig; }; order_notes_field: AudienceFieldConfig; + product_validation: { + [ key: string ]: ProductValidation; + }; };