Skip to content

Commit

Permalink
Merge pull request #2697 from woocommerce/fix/coupon-restrictions-for…
Browse files Browse the repository at this point in the history
…-brands

Ensure coupon brand restrictions are uploaded to Google Merchant Center.
  • Loading branch information
ianlin authored Dec 5, 2024
2 parents 3490c3d + 280a3ab commit 494e762
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 6 deletions.
88 changes: 87 additions & 1 deletion src/Coupon/SyncerHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
Expand Down Expand Up @@ -89,6 +90,13 @@ class SyncerHooks implements Service, Registerable {
*/
protected $wc;

/**
* WP Proxy
*
* @var WP
*/
protected WP $wp;

/**
* SyncerHooks constructor.
*
Expand All @@ -97,13 +105,15 @@ class SyncerHooks implements Service, Registerable {
* @param MerchantCenterService $merchant_center
* @param NotificationsService $notifications_service
* @param WC $wc
* @param WP $wp
*/
public function __construct(
CouponHelper $coupon_helper,
JobRepository $job_repository,
MerchantCenterService $merchant_center,
NotificationsService $notifications_service,
WC $wc
WC $wc,
WP $wp
) {
$this->update_coupon_job = $job_repository->get( UpdateCoupon::class );
$this->delete_coupon_job = $job_repository->get( DeleteCoupon::class );
Expand All @@ -112,6 +122,7 @@ public function __construct(
$this->merchant_center = $merchant_center;
$this->notifications_service = $notifications_service;
$this->wc = $wc;
$this->wp = $wp;
}

/**
Expand All @@ -138,6 +149,9 @@ public function register(): void {

// when a coupon is restored from trash, schedule a update job.
add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 );

// Update coupons when object terms get updated.
add_action( 'set_object_terms', [ $this, 'maybe_update_by_id_when_terms_updated' ], 90, 6 );
}

/**
Expand All @@ -152,6 +166,20 @@ public function update_by_id( int $coupon_id ) {
}
}

/**
* Update a coupon by the ID when the terms get updated.
*
* @param int $object_id The object ID.
* @param array $terms An array of object term IDs or slugs.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy The taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
* @param array $old_tt_ids Old array of term taxonomy IDs.
*/
public function maybe_update_by_id_when_terms_updated( int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids ) {
$this->handle_update_coupon_when_product_brands_updated( $taxonomy, $tt_ids, $old_tt_ids );
}

/**
* Delete a coupon by the ID
*
Expand Down Expand Up @@ -411,4 +439,62 @@ protected function handle_update_coupon_notification( WC_Coupon $coupon ) {
);
}
}

/**
* If product to brands relationship is updated, update the coupons that are related to the brands.
*
* @param string $taxonomy The taxonomy slug.
* @param array $tt_ids An array of term taxonomy IDs.
* @param array $old_tt_ids Old array of term taxonomy IDs.
*/
protected function handle_update_coupon_when_product_brands_updated( string $taxonomy, array $tt_ids, array $old_tt_ids ) {
if ( 'product_brand' !== $taxonomy ) {
return;
}

// Convert term taxonomy IDs to integers.
$tt_ids = array_map( 'intval', $tt_ids );
$old_tt_ids = array_map( 'intval', $old_tt_ids );

// Find the difference between the new and old term taxonomy IDs.
$diff1 = array_diff( $tt_ids, $old_tt_ids );
$diff2 = array_diff( $old_tt_ids, $tt_ids );
$diff = array_merge( $diff1, $diff2 );

if ( empty( $diff ) ) {
return;
}

// Serialize the diff to use in the meta query.
// This is needed because the meta value is serialized.
$serialized_diff = maybe_serialize( $diff );

$args = [
'post_type' => 'shop_coupon',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'product_brands',
'value' => $serialized_diff,
'compare' => 'LIKE',
],
[
'key' => 'exclude_product_brands',
'value' => $serialized_diff,
'compare' => 'LIKE',
],
],
];

// Get coupon posts based on the above query args.
$posts = $this->wp->get_posts( $args );

if ( empty( $posts ) ) {
return;
}

foreach ( $posts as $post ) {
$this->update_by_id( $post->ID );
}
}
}
53 changes: 50 additions & 3 deletions src/Coupon/WCCouponAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,25 @@ protected function map_wc_usage_restriction( WC_Coupon $wc_coupon ): WCCouponAda
$this->setItemId( $google_product_ids );
}

$wc_excluded_product_ids = $wc_coupon->get_excluded_product_ids();
if ( ! empty( $wc_excluded_product_ids ) ) {
$google_product_ids = array_map( $get_offer_id, $wc_excluded_product_ids );
// Currently the brand inclusion restriction will override the product inclustion restriction.
// It's align with the current coupon discounts behaviour in WooCommerce.
$wc_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon );
if ( ! empty( $wc_product_ids_in_brand ) ) {
$google_product_ids = array_map( $get_offer_id, $wc_product_ids_in_brand );
$has_product_restriction = true;
$this->setItemId( $google_product_ids );
}

// Get excluded product IDs and excluded product IDs in brand.
$wc_excluded_product_ids = $wc_coupon->get_excluded_product_ids();
$wc_excluded_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon, true );
if ( ! empty( $wc_excluded_product_ids ) || ! empty( $wc_excluded_product_ids_in_brand ) ) {
$google_product_ids = array_merge(
array_map( $get_offer_id, $wc_excluded_product_ids ),
array_map( $get_offer_id, $wc_excluded_product_ids_in_brand )
);
$google_product_ids = array_values( array_unique( $google_product_ids ) );

$has_product_restriction = true;
$this->setItemIdExclusion( $google_product_ids );
}
Expand Down Expand Up @@ -390,4 +406,35 @@ private function get_coupon_destinations( array $coupon_data ): array {

return apply_filters( 'woocommerce_gla_coupon_destinations', $destinations, $coupon_data );
}

/**
* Get the product IDs that belongs to a brand.
*
* @param WC_Coupon $wc_coupon The WC coupon object.
* @param bool $is_exclude If the product IDs are for exclusion.
* @return string[] The product IDs that belongs to a brand.
*/
private function get_product_ids_in_brand( WC_Coupon $wc_coupon, bool $is_exclude = false ) {
$coupon_id = $wc_coupon->get_id();
$meta_key = $is_exclude ? 'exclude_product_brands' : 'product_brands';

// Get the brand term IDs if brand restriction is set.
$brand_term_ids = get_post_meta( $coupon_id, $meta_key );

if ( ! is_array( $brand_term_ids ) ) {
return [];
}

$product_ids = [];
foreach ( $brand_term_ids as $brand_term_id ) {
// Get the product IDs that belongs to the brand.
$object_ids = get_objects_in_term( $brand_term_id, 'product_brand' );
if ( is_wp_error( $object_ids ) ) {
continue;
}
$product_ids = array_merge( $product_ids, $object_ids );
}

return $product_ids;
}
}
4 changes: 3 additions & 1 deletion src/Internal/DependencyManagement/JobServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Product;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping;
use Automattic\WooCommerce\GoogleListingsAndAds\Settings;

Expand Down Expand Up @@ -161,7 +162,8 @@ public function register(): void {
JobRepository::class,
MerchantCenterService::class,
NotificationsService::class,
WC::class
WC::class,
WP::class
);

$this->share_with_tags( StartProductSync::class, JobRepository::class );
Expand Down
56 changes: 55 additions & 1 deletion tests/Unit/Coupon/SyncerHooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\ContainerAwareUnitTest;
use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\CouponTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use PHPUnit\Framework\MockObject\MockObject;
use WC_Coupon;
use WC_Helper_Coupon;
use WC_Helper_Product;

/**
* Class SyncerHooksTest
Expand Down Expand Up @@ -55,6 +57,9 @@ class SyncerHooksTest extends ContainerAwareUnitTest {
/** @var WC $wc */
protected $wc;

/** @var WP $wp */
protected $wp;

/** @var SyncerHooks $syncer_hooks */
protected $syncer_hooks;

Expand Down Expand Up @@ -265,6 +270,51 @@ public function test_delete_coupon_triggers_notification_delete() {
$coupon->save();
}

public function test_product_brands_updated_schedules_update_job() {
// compatibility-code "WC < 9.4" -- Brands in core was added in WooCommerce 9.4
if ( version_compare( WC_VERSION, '9.4', '<' ) ) {
self::markTestSkipped( 'WooCommerce 9.4 or newer is needed to test WooCommerce Brands in core.' );
}

require_once WC_ABSPATH . '/includes/class-wc-brands.php';
\WC_Brands::init_taxonomy();

// Create products and brands.
/**
* @var WC_Product $product_1
*/
$product_1 = WC_Helper_Product::create_simple_product();
$product_2 = WC_Helper_Product::create_simple_product();
$brand_1 = wp_insert_term( 'Brand 1', 'product_brand' );

$brand_taxonomy = 'product_brand';

// Set the brand 1 for the product 1 and 2.
wp_set_post_terms( $product_1->get_id(), $brand_1['term_id'], $brand_taxonomy );
wp_set_post_terms( $product_2->get_id(), $brand_1['term_id'], $brand_taxonomy );

// Create a coupon.
/**
* @var WC_Coupon $coupon
*/
$coupon = WC_Helper_Coupon::create_coupon( uniqid() );
$coupon->set_status( 'publish' );
$coupon->add_meta_data( '_wc_gla_visibility', ChannelVisibility::SYNC_AND_SHOW, true );
// Add brand 1 to coupon inclusion restriction.
$coupon->add_meta_data( 'product_brands', [ (int) $brand_1['term_id'] ], true );
$coupon->save();

$this->set_mc_and_notifications();
$this->coupon_notification_job->expects( $this->never() )
->method( 'schedule' );
$this->update_coupon_job->expects( $this->once() )
->method( 'schedule' )
->with( $this->equalTo( [ [ $coupon->get_id() ] ] ) );

// Remove brand 1 from the product 1.
wp_set_object_terms( $product_1->get_id(), [], $brand_taxonomy );
}

public function test_actions_not_defined_when_mc_not_ready() {
$this->set_mc_and_notifications( false );

Expand All @@ -277,6 +327,7 @@ public function test_actions_not_defined_when_mc_not_ready() {
$this->assertFalse( has_action( 'deleted_post', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertFalse( has_action( 'woocommerce_delete_coupon', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertFalse( has_action( 'woocommerce_trash_coupon', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertfalse( has_action( 'set_object_terms', [ $this->syncer_hooks, 'maybe_update_by_id_when_terms_updated' ] ) );
}

public function test_actions_defined_when_mc_ready() {
Expand All @@ -291,6 +342,7 @@ public function test_actions_defined_when_mc_ready() {
$this->assertEquals( 90, has_action( 'deleted_post', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertEquals( 90, has_action( 'woocommerce_delete_coupon', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertEquals( 90, has_action( 'woocommerce_trash_coupon', [ $this->syncer_hooks, 'delete_by_id' ] ) );
$this->assertEquals( 90, has_action( 'set_object_terms', [ $this->syncer_hooks, 'maybe_update_by_id_when_terms_updated' ] ) );
}

/**
Expand All @@ -313,7 +365,8 @@ public function set_mc_and_notifications( bool $mc_status = true, bool $notifica
$this->job_repository,
$this->merchant_center,
$this->notification_service,
$this->wc
$this->wc,
$this->wp
);

$this->syncer_hooks->register();
Expand Down Expand Up @@ -349,6 +402,7 @@ public function setUp(): void {
);

$this->wc = $this->container->get( WC::class );
$this->wp = $this->container->get( WP::class );
$this->coupon_helper = $this->container->get( CouponHelper::class );
}
}
Loading

0 comments on commit 494e762

Please sign in to comment.