diff --git a/changelog/woopmnt-5531-move-spotlight-dismissal-logic-to-server b/changelog/woopmnt-5531-move-spotlight-dismissal-logic-to-server new file mode 100644 index 00000000000..42a4a7126e5 --- /dev/null +++ b/changelog/woopmnt-5531-move-spotlight-dismissal-logic-to-server @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Move spotlight dismissal/re-show logic to server side. diff --git a/client/data/promotions/types.d.ts b/client/data/promotions/types.d.ts index c965718f3cb..048919b3e31 100644 --- a/client/data/promotions/types.d.ts +++ b/client/data/promotions/types.d.ts @@ -19,21 +19,11 @@ export interface PromotionVariation { footnote?: string; } -export interface PromotionTypeConfig { - reshow_delay_days?: number; - max_dismissals?: number; -} - -export interface PromotionConfig { - [ key: string ]: PromotionTypeConfig | undefined; -} - export interface Promotion { promo_id: string; payment_method: string; discount_rate: string; duration_days: number; - config?: PromotionConfig; variations: PromotionVariation[]; } diff --git a/includes/class-wc-payments-pm-promotions-service.php b/includes/class-wc-payments-pm-promotions-service.php index 5a0c30c5493..f60f25fc09f 100644 --- a/includes/class-wc-payments-pm-promotions-service.php +++ b/includes/class-wc-payments-pm-promotions-service.php @@ -294,105 +294,25 @@ public function dismiss_promotion( string $identifier, string $variation_id ): a return $response; } - /** - * Filter variations based on config and dismissal history. - * - * @param array $promotions Array of promotions with variations. - * - * @return array Filtered promotions array. - */ - private function filter_variations_by_dismissals( array $promotions ): array { - foreach ( $promotions as &$promotion ) { - if ( empty( $promotion['variations'] ) ) { - continue; - } - - $promo_id = $promotion['promo_id']; - $variation_dismissals = self::get_promotion_variation_dismissals( $promo_id ); - - // Group variations by type to apply type-specific config. - $variations_by_type = []; - foreach ( $promotion['variations'] as $variation ) { - $type = $variation['type'] ?? 'default'; - if ( ! isset( $variations_by_type[ $type ] ) ) { - $variations_by_type[ $type ] = []; - } - $variations_by_type[ $type ][] = $variation; - } - - $filtered_variations = []; - - foreach ( $variations_by_type as $type => $type_variations ) { - // Get config for this variation type. - // Defaults: 1 dismissal allowed, no delay (must configure to show multiple variations). - $type_config = $promotion['config'][ $type ] ?? []; - $max_dismissals = $type_config['max_dismissals'] ?? 1; - $reshow_delay = $type_config['reshow_delay_days'] ?? 0; - $delay_seconds = $reshow_delay * DAY_IN_SECONDS; - - // Count dismissals for variations of this type. - $type_dismissals = 0; - $most_recent_dismissal = 0; - foreach ( $type_variations as $variation ) { - $dismissed_at = $variation_dismissals[ $variation['id'] ] ?? null; - if ( null !== $dismissed_at ) { - ++$type_dismissals; - if ( $dismissed_at > $most_recent_dismissal ) { - $most_recent_dismissal = $dismissed_at; - } - } - } - - // Check if max dismissals reached for this type. - if ( $type_dismissals >= $max_dismissals ) { - continue; - } - - // Check if still in delay period. - if ( $most_recent_dismissal > 0 && $delay_seconds > 0 ) { - $time_since_dismissal = time() - $most_recent_dismissal; - if ( $time_since_dismissal < $delay_seconds ) { - continue; - } - } - - // Find first non-dismissed variation of this type. - foreach ( $type_variations as $variation ) { - $dismissed_at = $variation_dismissals[ $variation['id'] ] ?? null; - if ( null === $dismissed_at ) { - $filtered_variations[] = $variation; - break; - } - } - } - - $promotion['variations'] = $filtered_variations; - } - unset( $promotion ); - - return $promotions; - } - /** * Get mock promotions data for testing. * TODO: Remove this method when server endpoints are available. * - * @return array Mock promotions data (array of promotions). + * The real server will receive dismissals via store_context and determine + * which variation (if any) to return based on dismissal history and timing. + * + * @return array Mock API response with promotions data. */ private function get_mock_promotions_data(): array { - // Mock available promotions with variations. + // Simple mock data with a single spotlight variation. + // The real server will handle filtering based on dismissals sent in store_context. $promotions = [ [ - 'promo_id' => 'klarna-2026-promo', - 'discount_rate' => '100%', - 'duration_days' => 90, - 'config' => [ - 'spotlight' => [ - 'reshow_delay_days' => 7, // Days to wait before showing next variation. - 'max_dismissals' => 2, // Total dismissals before permanent hide. - ], - ], - 'variations' => [ + 'promo_id' => 'klarna-2026-promo', + 'payment_method' => 'klarna', + 'discount_rate' => '100%', + 'duration_days' => 90, + 'variations' => [ [ 'id' => 'klarna-2026-promo__spotlight_primary', 'type' => 'spotlight', @@ -405,36 +325,6 @@ private function get_mock_promotions_data(): array { 'tc_url' => 'https://woocommerce.com/terms', 'footnote' => '*Terms and conditions apply. Offer valid for new customers only.', ], - [ - 'id' => 'klarna-2026-promo__spotlight_secondary', - 'type' => 'spotlight', - 'badge' => 'Last chance', - 'badge_type' => 'warning', - 'heading' => 'Final Reminder: Zero Processing Fees', - 'description' => 'Don\'t miss out! Get 0% processing fees for 90 days on all card payments', - 'cta_label' => 'Activate Now', - 'cta_url' => '#', - 'tc_url' => 'https://woocommerce.com/terms', - 'footnote' => '*Terms and conditions apply. Limited time offer.', - ], - ], - ], - [ - 'promo_id' => 'promo-affirm-cashback-2024', - 'discount_rate' => '2%', - 'duration_days' => 60, - 'variations' => [ - [ - 'id' => 'promo-affirm-cashback-2024__banner_primary', - 'type' => 'banner', - 'badge' => 'New', - 'badge_type' => 'info', - 'heading' => '2% Cashback on Affirm Transactions', - 'description' => 'Earn cashback on all Affirm payments for 60 days', - 'cta_label' => 'Learn More', - 'cta_url' => '#', - 'tc_url' => 'https://woocommerce.com/terms', - ], ], ], ]; diff --git a/tests/unit/admin/test-class-wc-rest-payments-pm-promotions-controller.php b/tests/unit/admin/test-class-wc-rest-payments-pm-promotions-controller.php index d092cc8ff24..98ff271860f 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-pm-promotions-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-pm-promotions-controller.php @@ -183,4 +183,77 @@ public function test_get_promotion_activation_time() { $this->assertSame( $timestamp, $result ); $this->assertNull( WC_Payments_PM_Promotions_Service::get_promotion_activation_time( 'promo2' ) ); } + + /** + * Test that dismissing a promotion records the timestamp and clears cache. + */ + public function test_dismiss_promotion_records_timestamp_and_clears_cache() { + // First, get promotions to populate cache. + $this->controller->get_promotions(); + + // Verify cache is set. + $cache_before = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY ); + $this->assertNotFalse( $cache_before, 'Cache should be set after getting promotions' ); + + // Dismiss the primary spotlight. + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'identifier', 'klarna-2026-promo' ); + $request->set_param( 'variation_id', 'klarna-2026-promo__spotlight_primary' ); + + $response = $this->controller->dismiss_promotion( $request ); + $data = $response->get_data(); + + $this->assertTrue( $data['success'] ); + $this->assertSame( 'dismissed', $data['status'] ); + + // Verify dismissal was recorded. + $dismissal_time = WC_Payments_PM_Promotions_Service::get_variation_dismissal_time( + 'klarna-2026-promo', + 'klarna-2026-promo__spotlight_primary' + ); + $this->assertNotNull( $dismissal_time ); + $this->assertEqualsWithDelta( time(), $dismissal_time, 5 ); // Within 5 seconds. + + // Verify cache was cleared. + $cache_after = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY ); + $this->assertFalse( $cache_after, 'Cache should be cleared after dismissal' ); + } + + /** + * Test that dismissals are included in store context for server requests. + * + * When dismissals change, the context hash changes, which triggers a fresh + * request to the server (instead of using cached data). + */ + public function test_dismissals_invalidate_cache_for_fresh_server_request() { + // Get promotions to populate cache. + $this->controller->get_promotions(); + + // Store the current cache. + $cache_before = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY ); + $this->assertNotFalse( $cache_before ); + $hash_before = $cache_before['context_hash']; + + // Add a dismissal (simulating what happens after dismiss_promotion clears cache). + $dismissals = [ + 'klarna-2026-promo' => [ + 'klarna-2026-promo__spotlight_primary' => time(), + ], + ]; + update_option( WC_Payments_PM_Promotions_Service::PROMOTION_DISMISSALS_OPTION, $dismissals ); + + // Clear cache and memo to simulate fresh request. + delete_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY ); + $this->promotions_service->reset_memo(); + + // Get promotions again - this should create a new cache with different hash. + $this->controller->get_promotions(); + + $cache_after = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY ); + $this->assertNotFalse( $cache_after ); + $hash_after = $cache_after['context_hash']; + + // The context hash should be different because dismissals changed. + $this->assertNotSame( $hash_before, $hash_after, 'Context hash should change when dismissals change' ); + } }