Add priority-based notice bar with milestone and extrapolation messaging#1150
Add priority-based notice bar with milestone and extrapolation messaging#1150danieliser merged 10 commits intodevelopfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaces randomized integration/tier upgrade messaging in Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Upsell as Upsell
participant License as LicenseService
participant Integrations as IntegrationDetector
participant Tracking as TrackingService
Caller->>Upsell: generate_upgrade_message()
Upsell->>License: is_pro_plus_with_valid_license?
License-->>Upsell: yes/no
alt pro_plus
Upsell-->>Caller: ""
else not_pro_plus
Upsell->>Integrations: detect_integrations()/active_extensions?
Integrations-->>Upsell: active_extensions_count
alt has_active_extensions
Upsell-->>Caller: ""
else no_active_extensions
Upsell->>Tracking: get_form_conversion_count()
Tracking-->>Upsell: conversion_count (or 0)
Upsell->>Upsell: get_notice_bar_triggers(conversion_count,...)
Upsell->>Upsell: rsort_by_priority(groups & triggers)
Upsell->>Upsell: get_current_notice_bar_trigger()
alt trigger_found
Upsell-->>Caller: selected_trigger.message
else none
Upsell-->>Caller: ""
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@classes/Upsell.php`:
- Line 237: Remove the unused dead variable $pro_is_active from the method in
classes/Upsell.php: delete the assignment "$pro_is_active =
\PopupMaker\plugin()->is_pro_active();" (and any related unused references) so
the method no longer creates an unused local; if logic depended on that check,
instead reference \PopupMaker\plugin()->is_pro_active() inline where actually
needed or add a meaningful use, but if not required simply remove the
assignment.
🧹 Nitpick comments (1)
classes/Upsell.php (1)
488-500: Consider catching\Throwablefor broader error coverage.PHP 7+ can throw
\Errorin addition to\Exception. Using\Throwablewould catch both. However, since the fallback is graceful (returns 0), this is a minor concern.♻️ Optional improvement
private static function get_form_conversion_count() { try { $form_tracking = \PopupMaker\plugin( 'form_conversion_tracking' ); if ( $form_tracking && method_exists( $form_tracking, 'get_site_count' ) ) { return $form_tracking->get_site_count(); } - } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Service not available, return 0. unset( $e ); // Prevent unused variable warning. } return 0; }
68c51ed to
0f210dd
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@classes/Upsell.php`:
- Around line 19-23: The file uses old array() syntax in the static hook
registrations (calls to add_filter/add_action) which triggers PHPCS
Generic.Arrays.DisallowLongArraySyntax; replace array( __CLASS__, 'addon_tabs' )
and similar occurrences (array( __CLASS__, 'popup_promotional_fields' ), array(
__CLASS__, 'theme_promotional_fields' ), array( __CLASS__, 'notice_bar_display'
)) with the short-array callable syntax [ __CLASS__, 'methodName' ] throughout
the Upsell class, or run vendor/bin/phpcbf classes/Upsell.php to auto-fix;
ensure all other array() usages in this file are converted to [] as well.
🧹 Nitpick comments (2)
classes/Upsell.php (2)
205-206: Side effect in getter method: consider separating read from write.
get_current_notice_bar_trigger()updates user meta as a side effect when returning a milestone trigger. This could cause unexpected behavior if the method is called multiple times or in contexts where side effects aren't expected (e.g., testing, previews).Consider separating the "get trigger" logic from "record that milestone was shown" into distinct methods, or document this behavior clearly in the method's docblock.
493-496: Minor: Simplify unused exception handling.Instead of
unset($e)to suppress the warning, you can avoid assigning the exception entirely:♻️ Suggested simplification
- } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - // Service not available, return 0. - unset( $e ); // Prevent unused variable warning. - } + } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Service not available, return 0. + }
…messaging Implements comprehensive upsell enhancement system with 4 priority groups: **Priority System:** - Milestone Achievements (pri: 100) - Celebration messages for success - Conversion Extrapolation (pri: 80) - Data-driven "you could have X more" - Integration Detected (pri: 60) - Contextual plugin-based messages - Generic Upgrade (pri: 40) - Fallback messages **New Milestone Triggers:** - First form conversion celebration (🎉 with 300-500% increase stat) - High engagement tiers (1K, 5K, 10K+ popup views with analytics pitch) **New Extrapolation Messages:** - Ecommerce: "You've tracked X conversions, capture ~Y more with Exit Intent" - LMS: "You've tracked X enrollments, capture ~Y more with LMS targeting" - Uses 33% extrapolation for ecommerce, 40% for LMS **Integration:** - Connects to FormConversionTracking service for milestone data - Uses existing popup view count for engagement milestones - Maintains backward compatibility with existing integration detection **Technical Changes:** - Added get_notice_bar_triggers() with Review Request pattern - Added get_current_notice_bar_trigger() for priority selection - Added get_form_conversion_count() helper with graceful fallback - Added rsort_by_priority() for consistent sorting - Deprecated get_integration_messages(), get_pro_integration_message(), get_free_upgrade_message() - All messages use generate_upgrade_url() for UTM tracking **Impact:** - Success-triggered messaging (not frustration-based) - Data-driven value propositions with concrete numbers - Foundation for future A/B testing and message optimization
**Messaging Accuracy:** - Changed "conversions" → "form submissions" (free plugin doesn't track specific conversion types) - Changed "enrollments" → "form submissions" for LMS (specific tracking is Pro feature) - Updated translator comments to reflect accurate terminology **Milestone Cooldown:** - Added 90-day cooldown for milestone achievement messages (seasonal re-engagement) - Tracks last shown milestone via user meta: _pum_last_milestone_trigger, _pum_last_milestone_at - Prevents showing same milestone repeatedly within cooldown period - Allows different milestones to show (e.g., first form → high engagement) - Non-milestone messages (integration detected, generic upgrade) unaffected **Impact:** - More accurate messaging that doesn't over-promise on free plugin tracking - Quarterly milestone re-engagement prevents message fatigue - Natural progression: celebration → extrapolation → generic
0f210dd to
a8b7c8f
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
classes/Upsell.php (2)
180-207: Consider defensive validation for user ID.
get_current_user_id()returns0for non-logged-in users. While admin context typically ensures authentication, adding a guard improves robustness and prevents silent failures in edge cases (e.g., REST API calls, cron contexts).Additionally, the
update_user_meta()calls on lines 205-206 are a side effect during trigger selection—this is intentional for tracking, but worth documenting in the method's docblock since the method name suggests a pure getter.🛡️ Proposed defensive check
private static function get_current_notice_bar_trigger() { $triggers = self::get_notice_bar_triggers(); + $user_id = get_current_user_id(); + + // Skip milestone tracking for non-logged-in users. + if ( 0 === $user_id ) { + // Fall through to non-milestone triggers or return first matching. + foreach ( $triggers as $group_key => $group ) { + if ( 'milestone_achievements' === $group_key ) { + continue; + } + foreach ( $group['triggers'] as $trigger ) { + if ( ! in_array( false, $trigger['conditions'], true ) ) { + return $trigger; + } + } + } + return false; + } + - $last_milestone_key = get_user_meta( get_current_user_id(), '_pum_last_milestone_trigger', true ); - $last_milestone_at = get_user_meta( get_current_user_id(), '_pum_last_milestone_at', true ); + $last_milestone_key = get_user_meta( $user_id, '_pum_last_milestone_trigger', true ); + $last_milestone_at = get_user_meta( $user_id, '_pum_last_milestone_at', true );And update the
update_user_meta()calls to use$user_id.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 180 - 207, The get_current_notice_bar_trigger() function uses get_current_user_id() without validating it and performs update_user_meta() side-effects with that implicit ID; add a defensive guard that captures get_current_user_id() into a $user_id variable, verify $user_id is a positive int (return early if not), and replace direct get_current_user_id() calls in update_user_meta() (and the get_user_meta() reads) with $user_id; also update the method docblock to note that this getter mutates user meta for milestone tracking.
257-270: Strict equality may miss milestone if conversions batch-process.The condition
1 === $form_countonly triggers when exactly 1 conversion exists. If conversions are batch-imported or the count jumps from 0 to 2+, users miss this celebration milestone.Consider using
$form_count >= 1combined with a "has shown first conversion" flag in user meta, or accept this as intentional behavior for organically acquired first conversions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 257 - 270, The current condition for the 'first_form_conversion' milestone uses a strict equality (1 === $form_count) which misses cases where the count jumps from 0 to >1; change the condition to check for $form_count >= 1 and add a persistent "shown" flag check/update in user meta so the message only displays once (e.g., check and set a meta key like popup_maker_first_form_milestone_shown via get_user_meta/update_user_meta or your platform's equivalents inside the same logic that builds 'first_form_conversion'); update the 'conditions' entry for 'first_form_conversion' to include both ($form_count >= 1) and the inverse of the shown-flag, and mark the flag as true when the notice is shown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@classes/Upsell.php`:
- Around line 180-207: The get_current_notice_bar_trigger() function uses
get_current_user_id() without validating it and performs update_user_meta()
side-effects with that implicit ID; add a defensive guard that captures
get_current_user_id() into a $user_id variable, verify $user_id is a positive
int (return early if not), and replace direct get_current_user_id() calls in
update_user_meta() (and the get_user_meta() reads) with $user_id; also update
the method docblock to note that this getter mutates user meta for milestone
tracking.
- Around line 257-270: The current condition for the 'first_form_conversion'
milestone uses a strict equality (1 === $form_count) which misses cases where
the count jumps from 0 to >1; change the condition to check for $form_count >= 1
and add a persistent "shown" flag check/update in user meta so the message only
displays once (e.g., check and set a meta key like
popup_maker_first_form_milestone_shown via get_user_meta/update_user_meta or
your platform's equivalents inside the same logic that builds
'first_form_conversion'); update the 'conditions' entry for
'first_form_conversion' to include both ($form_count >= 1) and the inverse of
the shown-flag, and mark the flag as true when the notice is shown.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 05b314d1-64e9-4487-a748-7d4974030ca5
📒 Files selected for processing (1)
classes/Upsell.php
There was a problem hiding this comment.
🧹 Nitpick comments (2)
classes/Upsell.php (2)
249-251: Consider extracting the hardcoded date to a constant.The date
'2026-04-01'determines new vs existing install behavior. Extracting this to a class constant would improve maintainability and make the feature ship date self-documenting.+ /** + * Date when form conversion tracking shipped. + */ + const FORM_TRACKING_SHIP_DATE = '2026-04-01';Then use
self::FORM_TRACKING_SHIP_DATEat line 250.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 249 - 251, The comparison uses a hardcoded date string ('2026-04-01') in Upsell that should be extracted to a class constant; add a class constant (e.g. const FORM_TRACKING_SHIP_DATE = '2026-04-01';) to the Upsell class and replace the literal in the is_new_install calculation (currently using strtotime('2026-04-01')) with self::FORM_TRACKING_SHIP_DATE so the ship date is centralized and self-documenting.
228-232: Static cache may persist stale data within long-running requests.The
static $triggerscache is never invalidated. In typical page loads this is fine, but if this method is called before and after a form conversion happens (e.g., during an AJAX request that processes a form), the cached value won't reflect the new$form_count.Given this runs only in admin page loads (not AJAX handlers), this is likely acceptable. Consider adding a comment noting the cache assumption.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 228 - 232, The static cache variable $triggers in the Upsell class is never invalidated which can persist stale data across long-running requests; update the method that declares static $triggers to either invalidate the cache when relevant or (preferred) add a clear comment above the static $triggers declaration documenting the assumption that this method runs only during normal admin page loads (not AJAX/form-processing flows) so the static cache is safe, and mention that if the method is ever used in async/form-processing contexts the cache must be cleared or removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@classes/Upsell.php`:
- Around line 249-251: The comparison uses a hardcoded date string
('2026-04-01') in Upsell that should be extracted to a class constant; add a
class constant (e.g. const FORM_TRACKING_SHIP_DATE = '2026-04-01';) to the
Upsell class and replace the literal in the is_new_install calculation
(currently using strtotime('2026-04-01')) with self::FORM_TRACKING_SHIP_DATE so
the ship date is centralized and self-documenting.
- Around line 228-232: The static cache variable $triggers in the Upsell class
is never invalidated which can persist stale data across long-running requests;
update the method that declares static $triggers to either invalidate the cache
when relevant or (preferred) add a clear comment above the static $triggers
declaration documenting the assumption that this method runs only during normal
admin page loads (not AJAX/form-processing flows) so the static cache is safe,
and mention that if the method is ever used in async/form-processing contexts
the cache must be cleared or removed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d0e6c4c3-cb7a-421e-b070-5d12fa73b0da
📒 Files selected for processing (1)
classes/Upsell.php
- Remove conversion extrapolation group (mixed form data with ecommerce/LMS features incorrectly) - Add variant messages for ecommerce (carts, revenue, upsell), LMS (enrollment, targeting), and CRM (tagging, automation) - Randomize across all matching triggers instead of always showing the first match - Engagement milestones already exclusive via conditions (1K/5K/10K ranges don't overlap)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
classes/Upsell.php (1)
477-485: Consider logging caught exceptions in debug mode for observability.Graceful fallback to
0is good, but swallowing all\Throwablemakes troubleshooting hard when service wiring breaks.As per coding guidelines "Use WordPress debug logging features for error handling".🛠️ Suggested lightweight debug logging
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - // Service not available, graceful fallback. - unset( $e ); + // Service not available, graceful fallback. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'Popup Maker: form_conversion_tracking unavailable in PUM_Upsell::get_form_conversion_count().' ); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 477 - 485, The catch block currently swallows all \Throwable for the \PopupMaker\plugin('form_conversion_tracking') call and get_site_count usage; update the catch for (\Throwable $e) to log the exception details when WP debug logging is enabled (e.g. check defined('WP_DEBUG') && WP_DEBUG and/or WP_DEBUG_LOG) and emit a concise message including context (mention Upsell/get_site_count, $e->getMessage(), $e->getFile(), $e->getLine()) so failures are recorded while still returning the graceful fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@classes/Upsell.php`:
- Around line 172-175: Update the stale docblock on the Upsell class method that
returns the highest-priority notice bar trigger (e.g.,
getHighestPriorityNoticeBarTrigger) to remove any mention of milestone
cooldowns; the implementation no longer enforces a 90-day cooldown, so the
docblock should state that the method returns the highest-priority matching
trigger and describe any current conditions it checks, without referencing
cooldown behavior.
- Around line 226-229: The hardcoded cutoff date makes the new-install check in
Upsell.php unreachable now; replace the literal '2026-04-01' used in computing
$is_new_install with a configurable or dynamic cutoff (e.g., use an option like
get_option('pum_reviews_new_install_cutoff') or a relative calculation such as
strtotime('-30 days')) so installs can be evaluated correctly; update the logic
around $installed_on and $is_new_install to read the cutoff from that option or
compute it at runtime and ensure the check remains strtotime($installed_on) >
strtotime($cutoff) (keep variable names $installed_on and $is_new_install so
callers remain stable).
---
Nitpick comments:
In `@classes/Upsell.php`:
- Around line 477-485: The catch block currently swallows all \Throwable for the
\PopupMaker\plugin('form_conversion_tracking') call and get_site_count usage;
update the catch for (\Throwable $e) to log the exception details when WP debug
logging is enabled (e.g. check defined('WP_DEBUG') && WP_DEBUG and/or
WP_DEBUG_LOG) and emit a concise message including context (mention
Upsell/get_site_count, $e->getMessage(), $e->getFile(), $e->getLine()) so
failures are recorded while still returning the graceful fallback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 21c918c4-e2e8-48bd-b075-c3d28b2344c3
📒 Files selected for processing (1)
classes/Upsell.php
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
classes/Upsell.php (2)
470-478: Priority sorting is computed but not utilized in selection.The groups and triggers are sorted by priority (highest first), but
get_current_notice_bar_trigger()collects all matching triggers into a flat array and randomly selects one. This sorting computation has no effect on the final selection.Either remove the sorting to avoid unnecessary computation, or modify the selection logic to respect priority.
♻️ Option A: Remove unused sorting
- // Sort groups by priority (highest first). - uasort( $triggers, [ __CLASS__, 'rsort_by_priority' ] ); - - // Sort triggers within each group by priority (highest first). - foreach ( $triggers as $group_key => $group ) { - if ( ! empty( $group['triggers'] ) ) { - uasort( $triggers[ $group_key ]['triggers'], [ __CLASS__, 'rsort_by_priority' ] ); - } - } - return $triggers;♻️ Option B: Respect priority in selection (in get_current_notice_bar_trigger)
if ( empty( $matching ) ) { return false; } - // Randomize so users see different messages across page loads. - return $matching[ array_rand( $matching ) ]; + // Return first match (already sorted by priority, highest first). + return reset( $matching );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 470 - 478, The code currently sorts groups and triggers using Upsell::rsort_by_priority but then flattens matching triggers in get_current_notice_bar_trigger and picks one at random, so the priority sorting is unused; fix by updating get_current_notice_bar_trigger to honor priority (e.g., weight selection toward higher-priority entries or pick the highest-priority matching trigger first) instead of uniformly randomizing, or remove the uasort calls around $triggers if you opt to drop priority entirely; locate the uasort usages and the get_current_notice_bar_trigger method to implement the chosen approach so rsort_by_priority and the $triggers sort are either relied upon in selection or eliminated.
201-209: Docblock describes priority-based selection, but implementation uses randomization.The docblock states triggers are "sorted by group priority" and "sorted by trigger priority," implying priority-based selection. However,
get_current_notice_bar_trigger()collects all matching triggers and randomly selects one viaarray_rand(), making the priority sorting unused for selection purposes.Consider either:
- Updating the docblock to reflect the randomization behavior, or
- Changing
get_current_notice_bar_trigger()to select the first match (respecting priority order)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@classes/Upsell.php` around lines 201 - 209, The docblock for the notice-bar trigger API claims triggers are selected by priority, but Upsell::get_current_notice_bar_trigger() actually gathers all matching triggers and uses array_rand() to pick one at random; update behavior to be consistent: either (A) change get_current_notice_bar_trigger() to return the first matching trigger respecting the sorted group/trigger order (remove array_rand() and return the top-priority match after the existing sort), or (B) change the docblock on Upsell::get_current_notice_bar_trigger() (and the return type description if necessary) to explicitly state that a random matching trigger is chosen and priority sorting is not used for selection — pick one of these fixes and apply it consistently in the Upsell class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@classes/Upsell.php`:
- Around line 470-478: The code currently sorts groups and triggers using
Upsell::rsort_by_priority but then flattens matching triggers in
get_current_notice_bar_trigger and picks one at random, so the priority sorting
is unused; fix by updating get_current_notice_bar_trigger to honor priority
(e.g., weight selection toward higher-priority entries or pick the
highest-priority matching trigger first) instead of uniformly randomizing, or
remove the uasort calls around $triggers if you opt to drop priority entirely;
locate the uasort usages and the get_current_notice_bar_trigger method to
implement the chosen approach so rsort_by_priority and the $triggers sort are
either relied upon in selection or eliminated.
- Around line 201-209: The docblock for the notice-bar trigger API claims
triggers are selected by priority, but Upsell::get_current_notice_bar_trigger()
actually gathers all matching triggers and uses array_rand() to pick one at
random; update behavior to be consistent: either (A) change
get_current_notice_bar_trigger() to return the first matching trigger respecting
the sorted group/trigger order (remove array_rand() and return the top-priority
match after the existing sort), or (B) change the docblock on
Upsell::get_current_notice_bar_trigger() (and the return type description if
necessary) to explicitly state that a random matching trigger is chosen and
priority sorting is not used for selection — pick one of these fixes and apply
it consistently in the Upsell class.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 80702742-9d45-490f-985b-a3fed0a5e669
📒 Files selected for processing (1)
classes/Upsell.php
- Fix docblock to describe randomized selection (not cooldown) - Use pum_installed_on option with 2026-03-25 cutoff for new vs existing install detection - Remove unused priority sorting (randomized selection doesn't need it) - Remove orphaned rsort_by_priority method
Summary
Implements comprehensive upsell enhancement system with intelligent priority-based trigger selection inspired by the Review Request module pattern.
Priority System
4 Priority Groups (highest to lowest):
New Milestone Triggers
New Extrapolation Messages
Milestone Cooldown System
_pum_last_milestone_trigger,_pum_last_milestone_atTechnical Implementation
get_notice_bar_triggers()- Priority-based trigger definition systemget_current_notice_bar_trigger()- Intelligent trigger selection with cooldown logicget_form_conversion_count()- Helper with graceful fallbackrsort_by_priority()- Consistent priority sortinggenerate_upgrade_url()for UTM trackingImpact
Testing Scenarios
Files Changed
classes/Upsell.php- Complete refactor of message generation system (2 commits)Commits
b283d213- feat: add priority-based notice bar with milestone and extrapolation messaging68c51ed5- fix: improve messaging accuracy and add milestone cooldown system🤖 Ready for CodeRabbit review
Summary by CodeRabbit
Refactor
Behavior Change
Bug Fix