Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: dev

Move spotlight dismissal/re-show logic to server side.
10 changes: 0 additions & 10 deletions client/data/promotions/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

Expand Down
132 changes: 11 additions & 121 deletions includes/class-wc-payments-pm-promotions-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
],
],
],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
}
}
Loading