Skip to content

Reminder to recurring contributor#1631

Open
tarunnjoshi wants to merge 1 commit intodevfrom
send-reminder-to-recurring-contribution
Open

Reminder to recurring contributor#1631
tarunnjoshi wants to merge 1 commit intodevfrom
send-reminder-to-recurring-contribution

Conversation

@tarunnjoshi
Copy link
Copy Markdown
Member

@tarunnjoshi tarunnjoshi commented Apr 3, 2026

Reminder to recurring contributor

Summary by CodeRabbit

  • New Features

    • Expanded subscription reminder processing to separately handle team-based and general recurring subscriptions in a unified workflow
    • Dynamic email template selection based on subscription type ensures appropriate messaging for each reminder
  • Improvements

    • Enhanced duplicate reminder prevention to eliminate redundant notifications
    • Optimized reminder suppression when subscription payments are successfully received

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Walkthrough

The Team5000SubscriptionReminderService was refactored to handle both Team 5000 and generic recurring reminder processing. The main processReminders() method now invokes two separate workflows, with Team 5000 logic extracted into a dedicated method. Methods were generalized to support both reminder types through additional parameters and conditional logic.

Changes

Cohort / File(s) Summary
Reminder Processing Logic
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php
Refactored processReminders() into two sequential flows via new processTeam5000Reminders() and processGenericRecurringReminders() methods. Generic flow selects active "In Progress" recurs with installments > 0, excluding Team 5000-handled IDs to prevent duplicates.
Subscription Processing Generalization
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php
Extended processSubscription() with ?string $contributionPageName and bool $isTeam5000 parameters to conditionally apply Team 5000-specific logic (renewal detection scope, email template selection).
Renewal & Payment Verification
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php
Generalized hasRenewed() renewal check to accept expiring start date and contribution page; added hasPaymentReceivedAfterReminder() to suppress duplicate reminders when Completed contributions arrive post-reminder activity.
Email Template Routing
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php
Updated sendReminderAndLog() to select between getReminderEmailHtml() (Team 5000) and new getGenericReminderEmailHtml() (generic) based on isTeam5000 flag.

Sequence Diagram(s)

sequenceDiagram
    participant Service as Team5000SubscriptionReminderService
    participant DB as Database
    participant Email as Email System
    
    Service->>Service: processReminders()
    
    par Team 5000 Flow
        Service->>DB: Query Team 5000 active recurs<br/>(specific contribution page)
        DB-->>Service: Team 5000 recur records
        Service->>DB: Check each recur for renewal
        DB-->>Service: Has newer recur started?
        Service->>Service: processTeam5000Reminders()
        Service->>Service: processSubscription(T5k page, isTeam5000=true)
        Service->>Email: sendReminderAndLog<br/>(getReminderEmailHtml)
        Email-->>Service: Email sent
    and Generic Flow
        Service->>DB: Query generic active recurs<br/>(In Progress, installments > 0)
        DB-->>Service: Generic recur records<br/>(exclude Team 5000 IDs)
        Service->>Service: processGenericRecurringReminders()
        Service->>Service: processSubscription(NULL page, isTeam5000=false)
        Service->>DB: Check payment received<br/>after reminder activity
        DB-->>Service: Latest contribution time
        Service->>Email: sendReminderAndLog<br/>(getGenericReminderEmailHtml)
        Email-->>Service: Email sent
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Subscription reminder #1624: Introduces renewal-exclusion logic that is now generalized and integrated into this PR's unified renewal-check mechanism serving both Team 5000 and generic flows.
  • Subscription expire reminder #1609: Previous refactoring of processReminders() flow; this PR extends it by splitting Team 5000 logic into a dedicated method and introducing parallel generic reminder processing.

Suggested labels

status : ready for review

Suggested reviewers

  • pokhiii

Poem

🔄 Two paths diverged in reminder land,
One Team 5000, one generic and grand,
Split yet synchronized, each finds its way,
Renewals and payments now properly weigh. ✉️


⚠️ Note on Best Practices Review:

While the refactoring appropriately separates concerns, watch for:

  • Potential duplicate logic between processTeam5000Reminders() and processGenericRecurringReminders() iteration patterns—consider extracting a shared loop abstraction if they diverge only in filtering criteria
  • Parameter proliferation in processSubscription() with $contributionPageName and $isTeam5000—verify this doesn't obscure intent or become error-prone with multiple conditional branches
  • Renewal check complexity now spanning expiry dates, contribution pages, and "later start_date" comparisons—ensure this logic is well-tested across both flows to prevent false negatives/positives
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Reminder to recurring contributor' is vague and generic, using broad terminology that doesn't convey the specific technical changes made to the codebase. Provide a more specific title that captures the main technical change, such as 'Split Team 5000 and generic recurring reminder processing' or 'Add generic recurring contribution reminders alongside Team 5000 flow'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch send-reminder-to-recurring-contribution

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php (4)

39-58: ⚠️ Potential issue | 🔴 Critical

Critical: is_test = TRUE filters out all production contributions.

Lines 43 and 57 filter for is_test = TRUE, meaning this code will only process test contributions and completely skip real donor data. This is almost certainly a debugging artifact that should not be in production.

🐛 Proposed fix
     $contributions = Contribution::get(FALSE)
       ->addSelect('contribution_recur_id')
       ->addWhere('contribution_page_id:name', '=', self::CONTRIBUTION_PAGE_NAME)
       ->addWhere('contribution_recur_id', 'IS NOT NULL')
-      ->addWhere('is_test', '=', TRUE)
+      ->addWhere('is_test', '=', FALSE)
       ->execute();
     $recurringContributions = ContributionRecur::get(FALSE)
       ->addSelect('id', 'contact_id', 'start_date', 'installments', 'frequency_interval', 'frequency_unit', 'amount', 'contribution_status_id')
       ->addWhere('id', 'IN', $recurIds)
       ->addWhere('contribution_status_id:name', '=', 'In Progress')
-      ->addWhere('is_test', '=', TRUE)
+      ->addWhere('is_test', '=', FALSE)
       ->execute();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 39 - 58, The queries for recurring contributions are incorrectly
filtering only test data: remove the is_test = TRUE filter (or change it to
is_test = FALSE if you explicitly want non-test records) from both the
Contribution::get() chain (where addWhere('is_test', '=', TRUE) appears) and the
ContributionRecur::get() chain (where addWhere('is_test', '=', TRUE) appears) so
that Contribution::get, the $recurIds extraction, and
ContributionRecur::get(...) will operate on real production contributions; keep
the other addWhere conditions (contribution_page_id, contribution_recur_id,
contribution_status_id) unchanged.

374-379: ⚠️ Potential issue | 🟠 Major

Email subject hardcoded to "Team 5000 Membership" for all flows.

Generic recurring donors will receive emails with subjects like "Your Team 5000 Membership Expires Tomorrow" — confusing for donors who aren't Team 5000 members.

🛠️ Suggested fix
-  private static function getEmailSubject(int $daysBefore): string {
+  private static function getEmailSubject(int $daysBefore, bool $isTeam5000 = TRUE): string {
+    $membershipType = $isTeam5000 ? 'Team 5000 Membership' : 'Recurring Donation';
     if ($daysBefore === 1) {
-      return 'Last Reminder: Your Team 5000 Membership Expires Tomorrow';
+      return "Last Reminder: Your {$membershipType} Expires Tomorrow";
     }
-    return "Reminder: Your Team 5000 Membership Expires in {$daysBefore} Days";
+    return "Reminder: Your {$membershipType} Expires in {$daysBefore} Days";
   }

Then pass $isTeam5000 from sendReminderAndLog().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 374 - 379, The email subject is hardcoded to "Team 5000 Membership"
in getEmailSubject(int $daysBefore) which mislabels non‑Team5000 donors; change
getEmailSubject to accept a boolean flag (e.g., getEmailSubject(int $daysBefore,
bool $isTeam5000)) and return a Team5000‑specific subject only when $isTeam5000
is true, otherwise return a generic subject (e.g., "Your Membership" or
flow‑appropriate text); update all call sites (notably sendReminderAndLog() and
any other callers) to pass the $isTeam5000 value through so the correct subject
is chosen per recipient.

127-173: ⚠️ Potential issue | 🟡 Minor

Log messages hardcoded to "Team 5000" for both flows.

The method now serves both Team 5000 and generic reminders, but log messages at lines 129, 152, 160-161, and 168 all say "Team 5000:". This will confuse log analysis when debugging generic reminder issues.

💡 Suggested approach
-  private static function processSubscription(array $recur, \DateTimeImmutable $now, string $from, ?string $contributionPageName, bool $isTeam5000): void {
+  private static function processSubscription(array $recur, \DateTimeImmutable $now, string $from, ?string $contributionPageName, bool $isTeam5000): void {
+    $logPrefix = $isTeam5000 ? 'Team 5000' : 'Recurring reminders';
     if (empty($recur['start_date']) || empty($recur['installments'])) {
-      \Civi::log()->info('Team 5000: Skipping recur {id} — missing start_date or installments', [
+      \Civi::log()->info($logPrefix . ': Skipping recur {id} — missing start_date or installments', [

Apply similar changes to other log statements in this method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 127 - 173, The log messages inside processSubscription are
hardcoded to "Team 5000" even when $isTeam5000 is false; update all log lines in
processSubscription (the info calls at/around the checks using
hasReminderBeenSent, hasPaymentReceivedAfterReminder, hasRenewed and the initial
missing-field log) to use a dynamic prefix or context string—e.g., compute
$logPrefix = $isTeam5000 ? 'Team 5000' : 'Reminder' (or include
$contributionPageName when present) and interpolate that prefix into each
Civi::log()->info message so logs correctly reflect which flow produced them.

360-369: ⚠️ Potential issue | 🟠 Major

Activity subject hardcoded to "Team 5000" for all reminder types.

Line 366 creates activities with subject "Team 5000 Subscription Expiry Reminder" regardless of whether this is a Team 5000 or generic reminder. This:

  1. Creates misleading audit trails for generic recurs
  2. Could cause hasPaymentReceivedAfterReminder() to behave unexpectedly if activity type filtering ever changes
🛠️ Suggested fix
-  private static function createReminderActivity(int $contactId, int $recurId, int $daysBefore): void {
+  private static function createReminderActivity(int $contactId, int $recurId, int $daysBefore, bool $isTeam5000 = TRUE): void {
+    $subjectPrefix = $isTeam5000 ? 'Team 5000 Subscription' : 'Recurring Donation';
     Activity::create(FALSE)
       ->addValue('activity_type_id:name', self::ACTIVITY_TYPE_NAME)
       ->addValue('status_id:name', 'Completed')
       ->addValue('source_contact_id', $contactId)
       ->addValue('target_contact_id', $contactId)
-      ->addValue('subject', 'Team 5000 Subscription Expiry Reminder - ' . $daysBefore . ' days (recur_id:' . $recurId . ')')
+      ->addValue('subject', $subjectPrefix . ' Expiry Reminder - ' . $daysBefore . ' days (recur_id:' . $recurId . ')')
       ->addValue('details', 'Automated reminder sent ' . $daysBefore . ' day(s) before subscription expiry.')
       ->execute();
   }

Then pass $isTeam5000 from sendReminderAndLog().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 360 - 369, The activity subject is hardcoded to "Team 5000" in
createReminderActivity; change createReminderActivity to accept a boolean flag
(e.g., $isTeam5000) and build the subject conditionally (use "Team 5000
Subscription Expiry Reminder" when $isTeam5000 is true, otherwise a generic
"Subscription Expiry Reminder"), preserving the daysBefore and recurId suffix;
update sendReminderAndLog to pass the $isTeam5000 value into
createReminderActivity and update any other callers of createReminderActivity
accordingly so the subject correctly reflects the reminder type.
🧹 Nitpick comments (2)
wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php (2)

81-101: Code duplication and inconsistent is_test filtering.

Two concerns here:

  1. Duplication: Lines 83-89 fetch Team 5000 contributions identically to lines 39-46. Consider extracting to a helper method.

  2. Missing is_test filter: The generic flow at line 91-101 doesn't filter on is_test, while Team 5000 does. This inconsistency could cause generic reminders to process test data in production.

♻️ Proposed fix
+  /**
+   * Returns Team 5000 recur IDs from contributions.
+   */
+  private static function getTeam5000RecurIds(): array {
+    $contributions = Contribution::get(FALSE)
+      ->addSelect('contribution_recur_id')
+      ->addWhere('contribution_page_id:name', '=', self::CONTRIBUTION_PAGE_NAME)
+      ->addWhere('contribution_recur_id', 'IS NOT NULL')
+      ->addWhere('is_test', '=', FALSE)
+      ->execute();
+
+    return array_unique(array_column((array) $contributions, 'contribution_recur_id'));
+  }

Then add the is_test filter to the generic query:

     $query = ContributionRecur::get(FALSE)
       ->addSelect('id', 'contact_id', 'start_date', 'installments', 'frequency_interval', 'frequency_unit', 'amount', 'contribution_status_id')
       ->addWhere('contribution_status_id:name', '=', 'In Progress')
+      ->addWhere('is_test', '=', FALSE)
       ->addWhere('installments', 'IS NOT NULL')
       ->addWhere('installments', '>', 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 81 - 101, The code in processGenericRecurringReminders duplicates
the Team 5000 contribution fetch (Contribution::get(...) block) and omits the
is_test filter present in the Team 5000 flow; extract the Team 5000 fetch into a
helper (e.g., getTeam5000RecurIds or similar) and reuse it in both places to
remove duplication, and add the same is_test filter to the generic
ContributionRecur query (the $query built from ContributionRecur::get(...) in
processGenericRecurringReminders) so test records are excluded consistently.

302-354: Template selection is correct; log messages need consistency.

The conditional template selection (lines 331-333) correctly chooses between Team 5000 and generic email templates based on $isTeam5000. However, the hardcoded "Team 5000:" log prefix at lines 313, 339, and 348 will mislead when processing generic reminders — same pattern noted earlier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`
around lines 302 - 354, The log prefix is hardcoded to "Team 5000:" in
sendReminderAndLog which mislabels generic reminders; introduce a local
$logPrefix = $isTeam5000 ? 'Team 5000' : 'Generic Reminder' at the top of
sendReminderAndLog and use "{$logPrefix}: ..." in the three logging calls (the
info skip when no email, the error on send failure, and the info on send
success) so logs reflect whether getReminderEmailHtml or
getGenericReminderEmailHtml was used; keep the existing context keys and calls
to \Civi::log(), \CRM_Utils_Mail::send, and self::createReminderActivity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`:
- Around line 39-58: The queries for recurring contributions are incorrectly
filtering only test data: remove the is_test = TRUE filter (or change it to
is_test = FALSE if you explicitly want non-test records) from both the
Contribution::get() chain (where addWhere('is_test', '=', TRUE) appears) and the
ContributionRecur::get() chain (where addWhere('is_test', '=', TRUE) appears) so
that Contribution::get, the $recurIds extraction, and
ContributionRecur::get(...) will operate on real production contributions; keep
the other addWhere conditions (contribution_page_id, contribution_recur_id,
contribution_status_id) unchanged.
- Around line 374-379: The email subject is hardcoded to "Team 5000 Membership"
in getEmailSubject(int $daysBefore) which mislabels non‑Team5000 donors; change
getEmailSubject to accept a boolean flag (e.g., getEmailSubject(int $daysBefore,
bool $isTeam5000)) and return a Team5000‑specific subject only when $isTeam5000
is true, otherwise return a generic subject (e.g., "Your Membership" or
flow‑appropriate text); update all call sites (notably sendReminderAndLog() and
any other callers) to pass the $isTeam5000 value through so the correct subject
is chosen per recipient.
- Around line 127-173: The log messages inside processSubscription are hardcoded
to "Team 5000" even when $isTeam5000 is false; update all log lines in
processSubscription (the info calls at/around the checks using
hasReminderBeenSent, hasPaymentReceivedAfterReminder, hasRenewed and the initial
missing-field log) to use a dynamic prefix or context string—e.g., compute
$logPrefix = $isTeam5000 ? 'Team 5000' : 'Reminder' (or include
$contributionPageName when present) and interpolate that prefix into each
Civi::log()->info message so logs correctly reflect which flow produced them.
- Around line 360-369: The activity subject is hardcoded to "Team 5000" in
createReminderActivity; change createReminderActivity to accept a boolean flag
(e.g., $isTeam5000) and build the subject conditionally (use "Team 5000
Subscription Expiry Reminder" when $isTeam5000 is true, otherwise a generic
"Subscription Expiry Reminder"), preserving the daysBefore and recurId suffix;
update sendReminderAndLog to pass the $isTeam5000 value into
createReminderActivity and update any other callers of createReminderActivity
accordingly so the subject correctly reflects the reminder type.

---

Nitpick comments:
In
`@wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php`:
- Around line 81-101: The code in processGenericRecurringReminders duplicates
the Team 5000 contribution fetch (Contribution::get(...) block) and omits the
is_test filter present in the Team 5000 flow; extract the Team 5000 fetch into a
helper (e.g., getTeam5000RecurIds or similar) and reuse it in both places to
remove duplication, and add the same is_test filter to the generic
ContributionRecur query (the $query built from ContributionRecur::get(...) in
processGenericRecurringReminders) so test records are excluded consistently.
- Around line 302-354: The log prefix is hardcoded to "Team 5000:" in
sendReminderAndLog which mislabels generic reminders; introduce a local
$logPrefix = $isTeam5000 ? 'Team 5000' : 'Generic Reminder' at the top of
sendReminderAndLog and use "{$logPrefix}: ..." in the three logging calls (the
info skip when no email, the error on send failure, and the info on send
success) so logs reflect whether getReminderEmailHtml or
getGenericReminderEmailHtml was used; keep the existing context keys and calls
to \Civi::log(), \CRM_Utils_Mail::send, and self::createReminderActivity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5bf52b63-f62f-4c63-999e-e365bd374c0b

📥 Commits

Reviewing files that changed from the base of the PR and between f752cb6 and f3dd604.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Team5000SubscriptionReminderService.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant