diff --git a/src/Harbor/Features/Resolve_Feature_Collection.php b/src/Harbor/Features/Resolve_Feature_Collection.php index d8f106ca..7579ebe4 100644 --- a/src/Harbor/Features/Resolve_Feature_Collection.php +++ b/src/Harbor/Features/Resolve_Feature_Collection.php @@ -249,10 +249,11 @@ private function resolve_capability_sources( Product_Collection $products, Catal } /** - * Returns the rank of the user's licensed tier for a product, or -1 if unlicensed. + * Returns the rank of the user's highest effective tier for a product, or -1 if unlicensed. * - * Returns -1 when the license is known to be ineffective for this domain, matching - * the resolve_capability_sources() guard so both flags are consistent. + * Only entries activated here and not known to be ineffective count, matching + * the resolve_capability_sources() guard so both flags are consistent. A product + * can have several entries, and an invalid one must not hide a valid one. * * @since 1.0.0 * @@ -262,19 +263,21 @@ private function resolve_capability_sources( Product_Collection $products, Catal * @return int The license tier rank, or -1 if no license covers this product. */ private function resolve_license_tier_rank( Product_Catalog $product, Product_Collection $products ): int { - $license = $products->get_activated_entry( $product->get_product_slug() ); + $rank = -1; - if ( null === $license ) { - return -1; - } + foreach ( $products->get_all_by_slug( $product->get_product_slug() ) as $license ) { + if ( ! $license->get_activated_here() || $this->is_license_invalid( $license->get_validation_status() ) ) { + continue; + } - if ( $this->is_license_invalid( $license->get_validation_status() ) ) { - return -1; - } + $tier = $product->get_tier_by_slug( $license->get_tier() ); - $tier = $product->get_tier_by_slug( $license->get_tier() ); + if ( $tier !== null ) { + $rank = max( $rank, $tier->get_rank() ); + } + } - return $tier !== null ? $tier->get_rank() : -1; + return $rank; } /** diff --git a/tests/wpunit/Features/Resolve_Feature_CollectionTest.php b/tests/wpunit/Features/Resolve_Feature_CollectionTest.php index 6ffee1da..bf4fa30e 100644 --- a/tests/wpunit/Features/Resolve_Feature_CollectionTest.php +++ b/tests/wpunit/Features/Resolve_Feature_CollectionTest.php @@ -361,6 +361,34 @@ public function test_paid_feature_without_legacy_or_unified_license_is_unavailab $this->assert_resolved_feature_flags( $resolver, 'kad-blocks-pro', false, false ); } + /** + * Tests that an invalid entry for a product does not hide a valid tier for the same product, + * whichever order Licensing returns them in. + * + * @return void + */ + public function test_invalid_entry_does_not_hide_a_valid_tier_of_the_same_product(): void { + $invalid = $this->make_product_entry( 'pro', [], 'license_banned' ); + $valid = $this->make_product_entry( 'basic', [ 'kad-blocks-pro' ] ); + + $orders = [ + 'invalid first' => [ $invalid, $valid ], + 'valid first' => [ $valid, $invalid ], + ]; + + foreach ( $orders as $label => $entries ) { + $products = new Product_Collection(); + foreach ( $entries as $entry ) { + $products->add( $entry ); + } + + $feature = ( $this->make_resolver( $this->make_catalog(), $products ) )()->get( 'kad-blocks-pro' ); + + $this->assertTrue( $feature->is_available(), $label ); + $this->assertTrue( $feature->is_in_catalog_tier(), $label ); + } + } + /** * Tests that an active legacy entry without `use_for_updates` opt-in does not grant availability. *