Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion includes/class-donations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
151 changes: 151 additions & 0 deletions includes/plugins/woocommerce/class-woocommerce-product-validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* WooCommerce Product Validator.
*
* @package Newspack
*/

namespace Newspack;

defined( 'ABSPATH' ) || exit;

/**
* Validates WooCommerce products for purchasability and restrictions.
*/
class WooCommerce_Product_Validator {
/**
* Validate if a product can be purchased by checking various conditions.
* This checks standard WooCommerce conditions and also WooCommerce Memberships restrictions.
*
* @param int|\WC_Product $product Product ID or product object.
*
* @return array|\WP_Error Array with validation results or WP_Error on failure.
*/
public static function validate_product_purchasability( $product ) {
if ( ! function_exists( 'wc_get_product' ) ) {
return new \WP_Error( 'woocommerce_inactive', __( 'WooCommerce inactive.', 'newspack-plugin' ) );
}
// Get the product object.
if ( ! is_a( $product, '\WC_Product' ) ) {
$product = wc_get_product( $product );
}

if ( ! $product ) {
return new \WP_Error( 'invalid_product', __( 'Invalid product.', 'newspack-plugin' ) );
}

// Basic WooCommerce checks.
$issues = self::validate_woocommerce_conditions( $product );

// WooCommerce Memberships checks.
if ( function_exists( 'wc_memberships' ) ) {
$issues = array_merge( $issues, self::validate_membership_restrictions( $product ) );
}

return [
'product_id' => $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;
}
}
47 changes: 47 additions & 0 deletions includes/wizards/audience/class-audience-donations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
44 changes: 43 additions & 1 deletion src/wizards/audience/views/donations/configuration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
<WizardsTab title={ __( 'Configuration', 'newspack-plugin' ) }>
{ /* Display product validation issues */ }
{ hasInvalidProducts ? (
<Notice
isWarning
noticeText={ __( 'Some donation products are invalid. Please correct the following issues:', 'newspack-plugin' ) }
style={ { marginBottom: '16px' } }
>
<ul style={ { marginTop: '8px', marginBottom: '0' } }>
{ validationResults.map( ( product: ProductValidation ) => {
if ( product.issues && product.issues.length > 0 ) {
return (
<li key={ product.product_id } style={ { marginBottom: '8px' } }>
<strong>
{ product.product_name ||
sprintf(
// translators: %d: Product ID.
__( 'Product ID %d', 'newspack-plugin' ),
product.product_id
) }
{ product.frequency && ` (${ product.frequency })` }:
</strong>{ ' ' }
<ExternalLink href={ `/wp-admin/post.php?post=${ product.product_id }&action=edit` }>
{ __( 'edit', 'newspack-plugin' ) }
</ExternalLink>
<ul style={ { marginTop: '4px', marginLeft: '20px' } }>
{ product.issues.map( ( warning: string, index: number ) => (
<li key={ index }>{ warning }</li>
) ) }
</ul>
</li>
);
}
return null;
} ) }
</ul>
</Notice>
) : null }

{ wizardData.donation_page && (
<>
<Card noBorder headerActions>
Expand Down
10 changes: 10 additions & 0 deletions src/wizards/types/hooks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] } }
Expand Down Expand Up @@ -97,4 +104,7 @@ type AudienceDonationsWizardData = {
[ key: string ]: AudienceFieldConfig;
};
order_notes_field: AudienceFieldConfig;
product_validation: {
[ key: string ]: ProductValidation;
};
};