Skip to content

Commit 0d4f1c7

Browse files
dmallory42claude
andcommitted
Move spotlight dismissal/re-show logic to server side
This removes client-side dismiss/wait/re-show logic in preparation for the WooPayments server handling this functionality. Changes: - Remove filter_variations_by_dismissals() method from promotions service - Simplify mock data to return a single spotlight variation - Remove PromotionTypeConfig and PromotionConfig from client types - Update tests to verify cache clearing and context hash invalidation The client now simply: 1. Sends dismissal timestamps to server via store_context 2. Displays whatever variation the server returns 3. Clears cache on dismissal to trigger fresh server request Fixes WOOPMNT-5531 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 6deef64 commit 0d4f1c7

File tree

4 files changed

+88
-131
lines changed

4 files changed

+88
-131
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: dev
3+
4+
Move spotlight dismissal/re-show logic to server side.

client/data/promotions/types.d.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,11 @@ export interface PromotionVariation {
1919
footnote?: string;
2020
}
2121

22-
export interface PromotionTypeConfig {
23-
reshow_delay_days?: number;
24-
max_dismissals?: number;
25-
}
26-
27-
export interface PromotionConfig {
28-
[ key: string ]: PromotionTypeConfig | undefined;
29-
}
30-
3122
export interface Promotion {
3223
promo_id: string;
3324
payment_method: string;
3425
discount_rate: string;
3526
duration_days: number;
36-
config?: PromotionConfig;
3727
variations: PromotionVariation[];
3828
}
3929

includes/class-wc-payments-pm-promotions-service.php

Lines changed: 11 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -294,105 +294,25 @@ public function dismiss_promotion( string $identifier, string $variation_id ): a
294294
return $response;
295295
}
296296

297-
/**
298-
* Filter variations based on config and dismissal history.
299-
*
300-
* @param array $promotions Array of promotions with variations.
301-
*
302-
* @return array Filtered promotions array.
303-
*/
304-
private function filter_variations_by_dismissals( array $promotions ): array {
305-
foreach ( $promotions as &$promotion ) {
306-
if ( empty( $promotion['variations'] ) ) {
307-
continue;
308-
}
309-
310-
$promo_id = $promotion['promo_id'];
311-
$variation_dismissals = self::get_promotion_variation_dismissals( $promo_id );
312-
313-
// Group variations by type to apply type-specific config.
314-
$variations_by_type = [];
315-
foreach ( $promotion['variations'] as $variation ) {
316-
$type = $variation['type'] ?? 'default';
317-
if ( ! isset( $variations_by_type[ $type ] ) ) {
318-
$variations_by_type[ $type ] = [];
319-
}
320-
$variations_by_type[ $type ][] = $variation;
321-
}
322-
323-
$filtered_variations = [];
324-
325-
foreach ( $variations_by_type as $type => $type_variations ) {
326-
// Get config for this variation type.
327-
// Defaults: 1 dismissal allowed, no delay (must configure to show multiple variations).
328-
$type_config = $promotion['config'][ $type ] ?? [];
329-
$max_dismissals = $type_config['max_dismissals'] ?? 1;
330-
$reshow_delay = $type_config['reshow_delay_days'] ?? 0;
331-
$delay_seconds = $reshow_delay * DAY_IN_SECONDS;
332-
333-
// Count dismissals for variations of this type.
334-
$type_dismissals = 0;
335-
$most_recent_dismissal = 0;
336-
foreach ( $type_variations as $variation ) {
337-
$dismissed_at = $variation_dismissals[ $variation['id'] ] ?? null;
338-
if ( null !== $dismissed_at ) {
339-
++$type_dismissals;
340-
if ( $dismissed_at > $most_recent_dismissal ) {
341-
$most_recent_dismissal = $dismissed_at;
342-
}
343-
}
344-
}
345-
346-
// Check if max dismissals reached for this type.
347-
if ( $type_dismissals >= $max_dismissals ) {
348-
continue;
349-
}
350-
351-
// Check if still in delay period.
352-
if ( $most_recent_dismissal > 0 && $delay_seconds > 0 ) {
353-
$time_since_dismissal = time() - $most_recent_dismissal;
354-
if ( $time_since_dismissal < $delay_seconds ) {
355-
continue;
356-
}
357-
}
358-
359-
// Find first non-dismissed variation of this type.
360-
foreach ( $type_variations as $variation ) {
361-
$dismissed_at = $variation_dismissals[ $variation['id'] ] ?? null;
362-
if ( null === $dismissed_at ) {
363-
$filtered_variations[] = $variation;
364-
break;
365-
}
366-
}
367-
}
368-
369-
$promotion['variations'] = $filtered_variations;
370-
}
371-
unset( $promotion );
372-
373-
return $promotions;
374-
}
375-
376297
/**
377298
* Get mock promotions data for testing.
378299
* TODO: Remove this method when server endpoints are available.
379300
*
380-
* @return array Mock promotions data (array of promotions).
301+
* The real server will receive dismissals via store_context and determine
302+
* which variation (if any) to return based on dismissal history and timing.
303+
*
304+
* @return array Mock API response with promotions data.
381305
*/
382306
private function get_mock_promotions_data(): array {
383-
// Mock available promotions with variations.
307+
// Simple mock data with a single spotlight variation.
308+
// The real server will handle filtering based on dismissals sent in store_context.
384309
$promotions = [
385310
[
386-
'promo_id' => 'klarna-2026-promo',
387-
'discount_rate' => '100%',
388-
'duration_days' => 90,
389-
'config' => [
390-
'spotlight' => [
391-
'reshow_delay_days' => 7, // Days to wait before showing next variation.
392-
'max_dismissals' => 2, // Total dismissals before permanent hide.
393-
],
394-
],
395-
'variations' => [
311+
'promo_id' => 'klarna-2026-promo',
312+
'payment_method' => 'klarna',
313+
'discount_rate' => '100%',
314+
'duration_days' => 90,
315+
'variations' => [
396316
[
397317
'id' => 'klarna-2026-promo__spotlight_primary',
398318
'type' => 'spotlight',
@@ -405,36 +325,6 @@ private function get_mock_promotions_data(): array {
405325
'tc_url' => 'https://woocommerce.com/terms',
406326
'footnote' => '*Terms and conditions apply. Offer valid for new customers only.',
407327
],
408-
[
409-
'id' => 'klarna-2026-promo__spotlight_secondary',
410-
'type' => 'spotlight',
411-
'badge' => 'Last chance',
412-
'badge_type' => 'warning',
413-
'heading' => 'Final Reminder: Zero Processing Fees',
414-
'description' => 'Don\'t miss out! Get 0% processing fees for 90 days on all card payments',
415-
'cta_label' => 'Activate Now',
416-
'cta_url' => '#',
417-
'tc_url' => 'https://woocommerce.com/terms',
418-
'footnote' => '*Terms and conditions apply. Limited time offer.',
419-
],
420-
],
421-
],
422-
[
423-
'promo_id' => 'promo-affirm-cashback-2024',
424-
'discount_rate' => '2%',
425-
'duration_days' => 60,
426-
'variations' => [
427-
[
428-
'id' => 'promo-affirm-cashback-2024__banner_primary',
429-
'type' => 'banner',
430-
'badge' => 'New',
431-
'badge_type' => 'info',
432-
'heading' => '2% Cashback on Affirm Transactions',
433-
'description' => 'Earn cashback on all Affirm payments for 60 days',
434-
'cta_label' => 'Learn More',
435-
'cta_url' => '#',
436-
'tc_url' => 'https://woocommerce.com/terms',
437-
],
438328
],
439329
],
440330
];

tests/unit/admin/test-class-wc-rest-payments-pm-promotions-controller.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,77 @@ public function test_get_promotion_activation_time() {
183183
$this->assertSame( $timestamp, $result );
184184
$this->assertNull( WC_Payments_PM_Promotions_Service::get_promotion_activation_time( 'promo2' ) );
185185
}
186+
187+
/**
188+
* Test that dismissing a promotion records the timestamp and clears cache.
189+
*/
190+
public function test_dismiss_promotion_records_timestamp_and_clears_cache() {
191+
// First, get promotions to populate cache.
192+
$this->controller->get_promotions();
193+
194+
// Verify cache is set.
195+
$cache_before = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY );
196+
$this->assertNotFalse( $cache_before, 'Cache should be set after getting promotions' );
197+
198+
// Dismiss the primary spotlight.
199+
$request = new WP_REST_Request( 'POST' );
200+
$request->set_param( 'identifier', 'klarna-2026-promo' );
201+
$request->set_param( 'variation_id', 'klarna-2026-promo__spotlight_primary' );
202+
203+
$response = $this->controller->dismiss_promotion( $request );
204+
$data = $response->get_data();
205+
206+
$this->assertTrue( $data['success'] );
207+
$this->assertSame( 'dismissed', $data['status'] );
208+
209+
// Verify dismissal was recorded.
210+
$dismissal_time = WC_Payments_PM_Promotions_Service::get_variation_dismissal_time(
211+
'klarna-2026-promo',
212+
'klarna-2026-promo__spotlight_primary'
213+
);
214+
$this->assertNotNull( $dismissal_time );
215+
$this->assertEqualsWithDelta( time(), $dismissal_time, 5 ); // Within 5 seconds.
216+
217+
// Verify cache was cleared.
218+
$cache_after = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY );
219+
$this->assertFalse( $cache_after, 'Cache should be cleared after dismissal' );
220+
}
221+
222+
/**
223+
* Test that dismissals are included in store context for server requests.
224+
*
225+
* When dismissals change, the context hash changes, which triggers a fresh
226+
* request to the server (instead of using cached data).
227+
*/
228+
public function test_dismissals_invalidate_cache_for_fresh_server_request() {
229+
// Get promotions to populate cache.
230+
$this->controller->get_promotions();
231+
232+
// Store the current cache.
233+
$cache_before = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY );
234+
$this->assertNotFalse( $cache_before );
235+
$hash_before = $cache_before['context_hash'];
236+
237+
// Add a dismissal (simulating what happens after dismiss_promotion clears cache).
238+
$dismissals = [
239+
'klarna-2026-promo' => [
240+
'klarna-2026-promo__spotlight_primary' => time(),
241+
],
242+
];
243+
update_option( WC_Payments_PM_Promotions_Service::PROMOTION_DISMISSALS_OPTION, $dismissals );
244+
245+
// Clear cache and memo to simulate fresh request.
246+
delete_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY );
247+
$this->promotions_service->reset_memo();
248+
249+
// Get promotions again - this should create a new cache with different hash.
250+
$this->controller->get_promotions();
251+
252+
$cache_after = get_transient( WC_Payments_PM_Promotions_Service::PROMOTIONS_CACHE_KEY );
253+
$this->assertNotFalse( $cache_after );
254+
$hash_after = $cache_after['context_hash'];
255+
256+
// The context hash should be different because dismissals changed.
257+
$this->assertNotSame( $hash_before, $hash_after, 'Context hash should change when dismissals change' );
258+
}
186259
}

0 commit comments

Comments
 (0)