Skip to content

Added cron to update induction status and send followup email#415

Merged
nishant22029 merged 11 commits intodevelopfrom
feat/induction-slot-follow-up-cron-job
Nov 6, 2024
Merged

Added cron to update induction status and send followup email#415
nishant22029 merged 11 commits intodevelopfrom
feat/induction-slot-follow-up-cron-job

Conversation

@nishant22029
Copy link
Copy Markdown
Collaborator

@nishant22029 nishant22029 commented Nov 5, 2024

Issue 137

please refer points:

  • If a volunteer does not select a slot within 7 (configurable) days of receiving the scheduling email, a follow-up email shall be sent.

  • If the volunteer does not respond to the follow-up email within 30 (configurable) days, their induction status in the CRM shall be changed to "No-show."

  • Created new cron job induction_slot_booking_follow_up_cron

    • Sends emails to contacts with unscheduled inductions over 7 days old.
    • Checks if an email was previously sent to avoid duplicates.
    • Uses a do-while loop to retrieve and process unscheduled induction activities in batches until all records are checked.
  • Created new cron job update_induction_status_no_show_cron

    • Retrieves a configurable message template used for sending follow-up emails.
    • In this cron job, a loop is used to process induction activities in batches of 25 records at a time.
    • Updates the status of induction activities to "No Show" if they are neither completed nor scheduled.

Summary by CodeRabbit

  • New Features

    • Introduced an API for managing follow-up actions related to induction slot bookings, including automated email notifications for unscheduled activities older than seven days.
    • Added an API for updating induction statuses to "No Show" for activities older than 30 days, with logging for follow-up email activities and status updates.
    • Enhanced the InductionService with methods for sending follow-up emails and updating induction statuses.
  • Bug Fixes

    • Implemented error handling to log exceptions and ensure smooth operation during follow-up and status updates.

@nishant22029 nishant22029 self-assigned this Nov 5, 2024
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 5, 2024

Walkthrough

This pull request introduces two new API files for managing induction slot bookings and updating induction statuses. The InductionSlotBookingFollowUpCron.php file implements a cron job that sends follow-up emails for unscheduled induction activities older than seven days. The UpdateInductionStatusNoShowCron.php file updates the status of induction activities to "No Show" for those older than thirty days. Both files include specifications for the API functions and incorporate error handling mechanisms.

Changes

File Path Change Summary
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php Added functions for managing follow-up actions related to induction slot bookings, including error handling.
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php Added functions for updating induction statuses to "No Show," with error handling and logging capabilities.
wp-content/civi-extensions/goonjcustom/Civi/InductionService.php Added methods for sending follow-up emails and updating induction statuses within the InductionService class.

Possibly related PRs

Suggested labels

status : ready for review

Suggested reviewers

  • pokhiii
  • tarunnjoshi

🎉 In the land of code where functions grow,
Two new cron jobs are set to flow.
Follow-ups sent, no shows updated,
With error logs neatly created.
A dance of emails, a status so bright,
Induction processes now take flight! ✈️


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (6)
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (3)

56-91: Ensure consistent batch processing without skipping records

The current batch processing logic increments the offset by the batch size ($offset += $batchSize), which may lead to records being skipped or processed multiple times if new records are added or existing ones are deleted during execution. This can cause inconsistent results in dynamic datasets.

To improve reliability, consider using a different batching strategy, such as processing based on the last processed ID or using a cursor. This helps ensure that all records are processed accurately, even if the underlying data changes during execution.


38-38: Enhance log messages for better traceability

The current log message 'Check for unscheduled induction activities' is generic. Enhancing log messages with more context, such as timestamps or identifiers, can aid in debugging and monitoring.

Consider updating the log message to include additional details.

Apply this diff to improve the log message:

 \Civi::log()->info('Check for unscheduled induction activities');
+\Civi::log()->info('Starting induction slot booking follow-up cron at ' . date('Y-m-d H:i:s'));

93-97: Catch specific exceptions instead of generic Exception

Catching the generic Exception class can obscure the actual exceptions that need handling and may make debugging more difficult. It's a best practice to catch specific exceptions that the code is expected to handle.

Consider catching more specific exceptions, such as CRM_Core_Exception.

Apply this diff to catch a more specific exception:

-} catch (Exception $e) {
+} catch (CRM_Core_Exception $e) {
     // Log any errors encountered during the process.
     \Civi::log()->error('Error in follow-up cron: ' . $e->getMessage());
     return civicrm_api3_create_error($e->getMessage());
 }
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php (3)

15-17: Consider adding a brief description of the purpose of this function.

While the function name is self-explanatory, adding a brief description of the purpose of this function can enhance the code's readability and maintainability.

/**
 * Goonjcustom.Induction Status Update to No Show API specification (optional)
 * This is used for documentation and validation.
 *
+ * This function defines the API specification for the Goonjcustom.update_induction_status_no_show_cron API.
+ * It specifies that there are no parameters for this cron job.
+ *
 * @param array $spec
 *   Description of fields supported by this API call.
 *
 * @return void
 */
function _civicrm_api3_goonjcustom_update_induction_status_no_show_cron_spec(&$spec) {
    // There are no parameters for the Goonjcustom cron.
}

38-39: Consider extracting the follow-up days as a constant or configuration variable.

Extracting the follow-up days as a constant or configuration variable can make it easier to adjust the value in the future and improve code readability.

+define('INDUCTION_STATUS_UPDATE_FOLLOW_UP_DAYS', 30);

-$followUpDays = 30;
+$followUpDays = INDUCTION_STATUS_UPDATE_FOLLOW_UP_DAYS;
 $followUpTimestamp = strtotime("-$followUpDays days");

41-42: Consider extracting the batch size as a constant or configuration variable.

Extracting the batch size as a constant or configuration variable can make it easier to adjust the value in the future and improve code readability.

+define('INDUCTION_STATUS_UPDATE_BATCH_SIZE', 25);

-$batchSize = 25;
+$batchSize = INDUCTION_STATUS_UPDATE_BATCH_SIZE;
 $offset = 0;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 9ae20a3 and ac2722d.

📒 Files selected for processing (2)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php (1 hunks)
🔇 Additional comments (4)
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php (4)

1-119: Excellent work on implementing the induction status update cron job!

The code is well-structured, follows best practices, and includes appropriate error handling. The use of API4 for data retrieval and updates is a great choice. The logging statements provide valuable insights into the execution flow and any issues encountered.

A few minor suggestions for further improvement:

  1. Consider extracting the batch size and follow-up days as constants or configuration variables to make them more easily adjustable.
  2. Add a brief description of the purpose and functionality of this cron job as a docblock comment at the beginning of the file.

Overall, this is a solid implementation that effectively addresses the requirements. Great job!


94-98: LGTM!

The code for updating the induction activity status to "No_show" is implemented correctly using the Activity API.


110-114: LGTM!

The error handling using a try-catch block and logging of errors is implemented correctly. Returning an error response using civicrm_api3_create_error is the appropriate approach.


116-118: LGTM!

The success logging and return of a success response using civicrm_api3_create_success is implemented correctly.

Comment on lines +49 to +54
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->setLimit(1)
->execute()
->single();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle the case where the email template is not found

The code assumes that the email template is always found. If the template is missing or misnamed, single() may return NULL, leading to a fatal error when accessing $template['msg_subject'].

Add a check to ensure the template exists before proceeding.

Apply this diff to handle the missing template scenario:

 $template = MessageTemplate::get(FALSE)
     ->addSelect('id', 'msg_subject')
     ->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
     ->setLimit(1)
     ->execute()
     ->single();

+if (!$template) {
+    throw new CRM_Core_Exception('Email template "Induction_slot_booking_follow_up_email" not found.');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->setLimit(1)
->execute()
->single();
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->setLimit(1)
->execute()
->single();
if (!$template) {
throw new CRM_Core_Exception('Email template "Induction_slot_booking_follow_up_email" not found.');
}

Comment on lines +68 to +86
foreach ($unscheduledInductionActivities as $activity) {
// Check if an followup email has already been sent to avoid duplication.
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->setLimit(1)
->execute()->first();

if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
$emailResult = civicrm_api3('Email', 'send', $emailParams);
\Civi::log()->info('Follow-up email sent', ['result' => $emailResult]);
}

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize database queries within the processing loop

Currently, the code performs a separate database query for each activity to check if a follow-up email has already been sent (Activity::get inside the loop). This can lead to performance issues due to the N+1 query problem, especially when processing a large number of activities.

To improve performance, consider retrieving all relevant email activities in a single query before the loop and using an in-memory lookup to check for existing emails.

Apply this diff to refactor the code:

 try {
     // Retrieve the email template for follow-up.
     $template = MessageTemplate::get(FALSE)
         ->addSelect('id', 'msg_subject')
         ->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
         ->setLimit(1)
         ->execute()
         ->single();

+    // Retrieve all contacts who have already received the follow-up email.
+    $contactsWithEmail = Activity::get(FALSE)
+        ->addSelect('source_contact_id')
+        ->addWhere('activity_type_id:name', '=', 'Email')
+        ->addWhere('subject', '=', $template['msg_subject'])
+        ->execute()
+        ->column('source_contact_id');
+
     do {
         // Retrieve a batch of unscheduled induction activities older than 7 days
         $unscheduledInductionActivities = Activity::get(FALSE)
             ->addSelect('id', 'source_contact_id', 'created_date')
             ->addWhere('activity_type_id:name', '=', 'Induction')
             ->addWhere('status_id:name', '=', 'To be scheduled')
             ->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
+            ->addWhere('source_contact_id', 'NOT IN', $contactsWithEmail)
             ->setLimit($batchSize)
             ->setOffset($offset)
             ->execute();

         // Process each activity in the batch
         foreach ($unscheduledInductionActivities as $activity) {
-            // Check if a follow-up email has already been sent to avoid duplication.
-            $emailActivities = Activity::get(FALSE)
-                ->addWhere('activity_type_id:name', '=', 'Email')
-                ->addWhere('subject', '=', $template['msg_subject'])
-                ->addWhere('source_contact_id', '=', $activity['source_contact_id'])
-                ->setLimit(1)
-                ->execute()->first();

-            if (!$emailActivities) {
                 $emailParams = [
                     'contact_id' => $activity['source_contact_id'],
                     'template_id' => $template['id'],
                 ];
                 $emailResult = civicrm_api3('Email', 'send', $emailParams);
                 \Civi::log()->info('Follow-up email sent', ['result' => $emailResult]);
-            }
         }

         // Move to the next batch by increasing the offset
         $offset += $batchSize;

     } while (count($unscheduledInductionActivities) === $batchSize);

 } catch (Exception $e) {
     // Log any errors encountered during the process.
     \Civi::log()->error('Error in follow-up cron: ' . $e->getMessage());
     return civicrm_api3_create_error($e->getMessage());
 }

Committable suggestion skipped: line range outside the PR's diff.

*/
function civicrm_api3_goonjcustom_induction_slot_booking_follow_up_cron($params) {
$returnValues = [];
\Civi::log()->info('Check for unscheduled induction activities');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not needed

Comment on lines +70 to +90
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()->single();

if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
$emailResult = civicrm_api3('Email', 'send', $emailParams);
\Civi::log()->info('Follow-up email sent', ['result' => $emailResult]);
}

}

// Move to the next batch by increasing the offset
$offset += $batchSize;

} while (count($unscheduledInductionActivities) === $batchSize);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Move the entire foreach logic to the InductionService file. Use the cron file solely for setting up the cron job, and transfer all functionality to the service file. Refer to CollectionCampCron.php as an example of how this structure is implemented.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure

'template_id' => $template['id'],
];
$emailResult = civicrm_api3('Email', 'send', $emailParams);
\Civi::log()->info('Follow-up email sent', ['result' => $emailResult]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not needed

$followUpDays = 7;

// Calculate the timestamp for 7 days ago from the current date
$followUpTimestamp = strtotime("-$followUpDays days");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does this return?

Copy link
Copy Markdown
Collaborator Author

@nishant22029 nishant22029 Nov 5, 2024

Choose a reason for hiding this comment

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

this line returns a Unix timestamp representing the date that is 7 days older than the current date.

->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
Copy link
Copy Markdown
Member

@tarunnjoshi tarunnjoshi Nov 5, 2024

Choose a reason for hiding this comment

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

If I create an unscheduled induction activity for Oct 29 and today’s date is November 5, will I receive a notification? Can you confirm this requirement and if yes then update the logic from < to <=

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

As per requirement its older than 7 days not equal to 7 days

->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Have you also noted the hours here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes

->addSelect('source_contact_id', 'activity_date_time')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('activity_type_id:label', '=', 'Email')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above

Comment on lines +69 to +79
// Fetch the associated induction activity
$inductionActivity = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'status_id:name')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()
->single();

if (!$inductionActivity) {
\Civi::log()->info('No induction activity found for source contact', [
'source_contact_id' => $activity['source_contact_id'],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Move the logic to the service file. The induction service as this code relates to induction

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure

->addWhere('id', '=', $inductionActivity['id'])
->execute();

\Civi::log()->info('Induction activity status updated to No_show', [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not needed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For monitoring logs in case any issue occur we can check logs

return civicrm_api3_create_error($e->getMessage());
}

\Civi::log()->info('Induction slot booking follow-up cron job completed successfully.');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not needed, please remove unnecessary logs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure

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.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (7)
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (3)

3-5: Remove unused imports

The Activity and MessageTemplate classes are no longer used directly in this file since the logic has been moved to InductionService. Consider removing these unused imports to maintain clean code.

-use Civi\Api4\Activity;
-use Civi\Api4\MessageTemplate;
 use Civi\InductionService;

7-18: Enhance API documentation

While the documentation follows the CiviCRM standard, it could be more descriptive about the cron job's purpose and execution frequency.

 /**
- * Goonjcustom.InductionSlotBookingFollowUp API specification (optional)
- * This is used for documentation and validation.
+ * Goonjcustom.InductionSlotBookingFollowUp API specification
+ * This API is designed to be called as a cron job to send follow-up emails
+ * for unscheduled induction activities.
  *
  * @param array $spec
  *   description of fields supported by this API call.
  *
  * @return void
  */
 function _civicrm_api3_goonjcustom_induction_slot_booking_follow_up_cron_spec(&$spec) {
-  // There are no parameters for the Goonjcustom cron.
+  // This cron job runs automatically and requires no input parameters.
 }

37-48: Enhance API response with meaningful information

The API currently returns an empty array on success. Consider returning meaningful information about the operation's results.

 function civicrm_api3_goonjcustom_induction_slot_booking_follow_up_cron($params) {
-    $returnValues = [];
+    $returnValues = ['processed' => false];
     try {
-        InductionService::sendFollowUpEmails();
+        $result = InductionService::sendFollowUpEmails();
+        $returnValues = [
+            'processed' => true,
+            'emails_sent' => $result['sent'] ?? 0,
+            'timestamp' => date('Y-m-d H:i:s'),
+        ];
     } catch (Exception $e) {
         // Log any errors encountered during the process.
         \Civi::log()->error('Error in follow-up cron: ' . $e->getMessage());
         return civicrm_api3_create_error($e->getMessage());
     }

     return civicrm_api3_create_success($returnValues, $params, 'Goonjcustom', 'induction_slot_booking_follow_up_cron');
 }
wp-content/civi-extensions/goonjcustom/Civi/InductionService.php (4)

505-505: Remove debug log statement

The debug log statement cjecka appears to be a development artifact and should be removed.

- \Civi::log()->info('cjecka');

458-461: Reduce code duplication in template retrieval

The email template retrieval logic is duplicated. Consider extracting it to a private method.

+ private static function getFollowUpEmailTemplate() {
+   return MessageTemplate::get(FALSE)
+     ->addSelect('id', 'msg_subject')
+     ->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
+     ->execute()->single();
+ }

Then use it in both methods:

- $template = MessageTemplate::get(FALSE)
-   ->addSelect('id', 'msg_subject')
-   ->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
-   ->execute()->single();
+ $template = self::getFollowUpEmailTemplate();

Also applies to: 513-516


532-567: Simplify complex nested logic

The nested logic for processing induction activities could be simplified by extracting the processing logic into a separate method.

+ private static function processInductionActivity($activity) {
+   $inductionActivity = Activity::get(FALSE)
+     ->addSelect('id', 'source_contact_id', 'status_id:name')
+     ->addWhere('activity_type_id:name', '=', 'Induction')
+     ->addWhere('source_contact_id', '=', $activity['source_contact_id'])
+     ->execute()
+     ->single();
+
+   if (!$inductionActivity) {
+     \Civi::log()->info('No induction activity found for source contact', [
+       'source_contact_id' => $activity['source_contact_id'],
+     ]);
+     return;
+   }
+
+   if (in_array($inductionActivity['status_id:name'], ['Completed', 'Scheduled'])) {
+     \Civi::log()->info('Induction activity is already completed or scheduled, skipping', [
+       'inductionActivityId' => $inductionActivity['id'],
+       'status' => $inductionActivity['status_id:name']
+     ]);
+     return;
+   }
+
+   $updateResult = Activity::update(FALSE)
+     ->addValue('status_id:name', 'No_show')
+     ->addWhere('id', '=', $inductionActivity['id'])
+     ->execute();
+
+   \Civi::log()->info('Induction activity status updated to No_show', [
+     'inductionActivityId' => $inductionActivity['id'],
+     'updateResult' => $updateResult
+   ]);
+ }

Then simplify the loop:

   foreach ($followUpEmailActivities as $activity) {
-    // Current complex nested logic
+    self::processInductionActivity($activity);
   }

450-578: Consider implementing rate limiting

Both methods process activities in batches but lack rate limiting for API calls. Consider adding a delay between batches to prevent overwhelming the system.

Consider adding a small delay between batches:

   } while (count($unscheduledInductionActivities) === $batchSize);
+  // Add a small delay between batches
+  usleep(100000); // 100ms delay
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between ac2722d and a654e78.

📒 Files selected for processing (3)
  • wp-content/civi-extensions/goonjcustom/Civi/InductionService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php
🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (1)

41-45: LGTM! Robust error handling implementation.

The error handling follows best practices by:

  • Using try-catch for exception handling
  • Properly logging errors with context
  • Returning standardized API error responses

Comment on lines +454 to +464
$followUpDays = 7;
$followUpTimestamp = strtotime("-{$followUpDays} days");

// Retrieve the email template for follow-up.
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->execute()->single();

$batchSize = 25;
$offset = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Convert magic numbers to configurable constants

The follow-up days and batch size should be defined as class constants for better maintainability and reusability.

+ const FOLLOW_UP_DAYS = 7;
+ const BATCH_SIZE = 25;
- $followUpDays = 7;
- $batchSize = 25;

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +486 to +493
if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for email sending operations

The email sending operation lacks error handling, which could lead to silent failures.

   if (!$emailActivities) {
     $emailParams = [
       'contact_id' => $activity['source_contact_id'],
       'template_id' => $template['id'],
     ];
-    civicrm_api3('Email', 'send', $emailParams);
+    try {
+      $result = civicrm_api3('Email', 'send', $emailParams);
+      if ($result['is_error']) {
+        \Civi::log()->error('Failed to send follow-up email', [
+          'contact_id' => $activity['source_contact_id'],
+          'error' => $result['error_message']
+        ]);
+      }
+    } catch (\Exception $e) {
+      \Civi::log()->error('Exception while sending follow-up email', [
+        'contact_id' => $activity['source_contact_id'],
+        'error' => $e->getMessage()
+      ]);
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}
if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
try {
$result = civicrm_api3('Email', 'send', $emailParams);
if ($result['is_error']) {
\Civi::log()->error('Failed to send follow-up email', [
'contact_id' => $activity['source_contact_id'],
'error' => $result['error_message']
]);
}
} catch (\Exception $e) {
\Civi::log()->error('Exception while sending follow-up email', [
'contact_id' => $activity['source_contact_id'],
'error' => $e->getMessage()
]);
}
}
}

*
*/
public static function updateInductionStatusNoShow() {
\Civi::log()->info('cjecka');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please remove unnecessary logs.

->addWhere('id', '=', $inductionActivity['id'])
->execute();

\Civi::log()->info('Induction activity status updated to No_show', [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you are using the try-catch, why are you adding logs everywhere? Let's use the catch block to fetch the necessary logs you want in case the code won't run.
Whenever this cron runs every time these logs print and I expect we don't need that we only need the logs when the code won't run right?

} while (count($followUpEmailActivities) === $batchSize);

} catch (\Exception $e) {
// Log any errors encountered during the process
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delete this comment—your code is shouting what it’s doing!

} catch (\Exception $e) {
// Log any errors encountered during the process
\Civi::log()->error('Error in updating induction status: ' . $e->getMessage());
throw $e; // Re-throw the exception to handle it at a higher level
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this comment too

try {
InductionService::sendFollowUpEmails();
} catch (Exception $e) {
// Log any errors encountered during the process.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delete this comment—your code is shouting what it’s doing! AND please remove these comments like add logs, Log any errors encountered during the process.

try {
InductionService::updateInductionStatusNoShow();
} catch (\Exception $e) {
// Log any errors encountered during the process
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

here

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.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/InductionService.php (3)

453-453: Add PHPDoc block for the method.

The method lacks documentation describing its purpose, parameters, and return value.

Add a descriptive PHPDoc block:

/**
 * Sends follow-up emails to volunteers who haven't scheduled their induction.
 * 
 * Processes unscheduled induction activities older than 7 days in batches,
 * sending follow-up emails if not already sent.
 * 
 * @return void
 */
public static function sendFollowUpEmails() {

504-504: Add PHPDoc block for the method.

The method lacks documentation describing its purpose, parameters, and return value.

Add a descriptive PHPDoc block:

/**
 * Updates induction status to "No_show" for activities that haven't been completed
 * or scheduled within 30 days after the follow-up email.
 * 
 * @throws \Exception When the follow-up email template is not found or on API errors
 * @return void
 */
public static function updateInductionStatusNoShow() {

541-554: Remove unnecessary logging of normal flow conditions.

The logging of normal flow conditions adds noise to the logs. Only exceptional conditions should be logged.

Remove or adjust the logging:

-     if (!$inductionActivity) {
-       \Civi::log()->info('No induction activity found for source contact', [
-         'source_contact_id' => $activity['source_contact_id'],
-       ]);
+     if (!$inductionActivity) {
        continue;
      }

      // Check if the status is 'Completed' or 'Scheduled'
      if (in_array($inductionActivity['status_id:name'], ['Completed', 'Scheduled'])) {
-       \Civi::log()->info('Induction activity is already completed or scheduled, skipping', [
-         'inductionActivityId' => $inductionActivity['id'],
-         'status' => $inductionActivity['status_id:name']
-       ]);
        continue;
      }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a654e78 and 17d7fc3.

📒 Files selected for processing (3)
  • wp-content/civi-extensions/goonjcustom/Civi/InductionService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/InductionSlotBookingFollowUpCron.php
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/UpdateInductionStatusNoShowCron.php
🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/Civi/InductionService.php (1)

454-456: Convert magic numbers to class constants.

Both methods use magic numbers for follow-up days and batch size.

This issue was already identified in a previous review. The suggestion to convert these to class constants is still valid.

Also applies to: 505-507

Comment on lines +458 to +461
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->execute()->single();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for API calls.

The API calls to MessageTemplate::get and Activity::get lack error handling. If these fail, the method will throw uncaught exceptions.

Add try-catch blocks:

    try {
      // Retrieve the email template for follow-up.
      $template = MessageTemplate::get(FALSE)
        ->addSelect('id', 'msg_subject')
        ->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
        ->execute()->single();

      if (!$template) {
        throw new \Exception('Follow-up email template not found.');
      }
+   } catch (\Exception $e) {
+     \Civi::log()->error('Error retrieving follow-up email template: ' . $e->getMessage());
+     throw $e;
+   }

    try {
      // Retrieve a batch of unscheduled induction activities older than 7 days
      $unscheduledInductionActivities = Activity::get(FALSE)
        ->addSelect('id', 'source_contact_id', 'created_date')
        ->addWhere('activity_type_id:name', '=', 'Induction')
        ->addWhere('status_id:name', '=', 'To be scheduled')
        ->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
        ->setLimit($batchSize)
        ->setOffset($offset)
        ->execute();
+   } catch (\Exception $e) {
+     \Civi::log()->error('Error retrieving unscheduled induction activities: ' . $e->getMessage());
+     throw $e;
+   }

Also applies to: 468-475

Comment on lines +453 to +571
public static function sendFollowUpEmails() {
$followUpDays = 7;
$followUpTimestamp = strtotime("-{$followUpDays} days");

// Retrieve the email template for follow-up.
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->execute()->single();

$batchSize = 25;
$offset = 0;

do {
// Retrieve a batch of unscheduled induction activities older than 7 days
$unscheduledInductionActivities = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
->setLimit($batchSize)
->setOffset($offset)
->execute();

// Process each activity in the batch
foreach ($unscheduledInductionActivities as $activity) {
// Check if a follow-up email has already been sent to avoid duplication.
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()->single();

if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}

// Move to the next batch by increasing the offset
$offset += $batchSize;

} while (count($unscheduledInductionActivities) === $batchSize);
}

/**
*
*/
public static function updateInductionStatusNoShow() {
$followUpDays = 30;
$followUpTimestamp = strtotime("-$followUpDays days");
$batchSize = 25;
$offset = 0;

try {
// Fetch the follow-up message template
$template = MessageTemplate::get(FALSE)
->addSelect('id', 'msg_subject')
->addWhere('msg_title', 'LIKE', 'Induction_slot_booking_follow_up_email%')
->execute()->single();

if (!$template) {
throw new \Exception('Follow-up email template not found.');
}

do {
// Fetch email activities older than 30 days
$followUpEmailActivities = Activity::get(TRUE)
->addSelect('source_contact_id', 'activity_date_time')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('activity_type_id:label', '=', 'Email')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
->setLimit($batchSize)
->setOffset($offset)->execute();

foreach ($followUpEmailActivities as $activity) {
// Fetch the associated induction activity
$inductionActivity = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'status_id:name')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()
->single();

if (!$inductionActivity) {
\Civi::log()->info('No induction activity found for source contact', [
'source_contact_id' => $activity['source_contact_id'],
]);
continue;
}

// Check if the status is 'Completed' or 'Scheduled'
if (in_array($inductionActivity['status_id:name'], ['Completed', 'Scheduled'])) {
\Civi::log()->info('Induction activity is already completed or scheduled, skipping', [
'inductionActivityId' => $inductionActivity['id'],
'status' => $inductionActivity['status_id:name']
]);
continue;
}

// Update the induction status to 'No_show'
$updateResult = Activity::update(FALSE)
->addValue('status_id:name', 'No_show')
->addWhere('id', '=', $inductionActivity['id'])
->execute();
}

// Increment the offset by the batch size
$offset += $batchSize;
} while (count($followUpEmailActivities) === $batchSize);

} catch (\Exception $e) {
\Civi::log()->error('Error in updating induction status: ' . $e->getMessage());
throw $e;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract common batch processing logic.

Both methods share similar batch processing logic. This violates the DRY principle.

Create a utility method for batch processing:

+ /**
+  * Process records in batches using a callback function.
+  *
+  * @param callable $fetchBatch Function to fetch a batch of records
+  * @param callable $processBatch Function to process each batch
+  * @param int $batchSize Size of each batch
+  * @return void
+  */
+ private static function processBatches(callable $fetchBatch, callable $processBatch, int $batchSize = 25): void {
+   $maxIterations = 1000;
+   $iteration = 0;
+   $offset = 0;
+
+   do {
+     $records = $fetchBatch($batchSize, $offset);
+     $processBatch($records);
+     $offset += $batchSize;
+     $iteration++;
+   } while (count($records) === $batchSize && $iteration < $maxIterations);
+
+   if ($iteration >= $maxIterations) {
+     \Civi::log()->warning('Maximum iterations reached in batch processing');
+   }
+ }

Then update both methods to use this utility:

  public static function sendFollowUpEmails() {
    // ... template retrieval ...
    
-   $batchSize = 25;
-   $offset = 0;
-   do {
-     // ... batch processing ...
-   } while (count($unscheduledInductionActivities) === $batchSize);
+   self::processBatches(
+     function ($batchSize, $offset) use ($followUpTimestamp) {
+       return Activity::get(FALSE)
+         ->addSelect('id', 'source_contact_id', 'created_date')
+         // ... existing conditions ...
+         ->setLimit($batchSize)
+         ->setOffset($offset)
+         ->execute();
+     },
+     function ($activities) use ($template) {
+       foreach ($activities as $activity) {
+         // ... existing processing ...
+       }
+     }
+   );
  }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +466 to +498
do {
// Retrieve a batch of unscheduled induction activities older than 7 days
$unscheduledInductionActivities = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
->setLimit($batchSize)
->setOffset($offset)
->execute();

// Process each activity in the batch
foreach ($unscheduledInductionActivities as $activity) {
// Check if a follow-up email has already been sent to avoid duplication.
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()->single();

if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}

// Move to the next batch by increasing the offset
$offset += $batchSize;

} while (count($unscheduledInductionActivities) === $batchSize);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add safety check to prevent infinite loop.

The do-while loop could potentially run indefinitely if the API consistently returns $batchSize records.

Add a maximum iteration limit:

+   $maxIterations = 1000; // Adjust based on expected maximum records
+   $iteration = 0;
    do {
      // ... existing code ...
      
      $offset += $batchSize;
+     $iteration++;
      
-   } while (count($unscheduledInductionActivities) === $batchSize);
+   } while (count($unscheduledInductionActivities) === $batchSize && $iteration < $maxIterations);
+   
+   if ($iteration >= $maxIterations) {
+     \Civi::log()->warning('Maximum iterations reached in sendFollowUpEmails');
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
do {
// Retrieve a batch of unscheduled induction activities older than 7 days
$unscheduledInductionActivities = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
->setLimit($batchSize)
->setOffset($offset)
->execute();
// Process each activity in the batch
foreach ($unscheduledInductionActivities as $activity) {
// Check if a follow-up email has already been sent to avoid duplication.
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()->single();
if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}
// Move to the next batch by increasing the offset
$offset += $batchSize;
} while (count($unscheduledInductionActivities) === $batchSize);
$maxIterations = 1000; // Adjust based on expected maximum records
$iteration = 0;
do {
// Retrieve a batch of unscheduled induction activities older than 7 days
$unscheduledInductionActivities = Activity::get(FALSE)
->addSelect('id', 'source_contact_id', 'created_date')
->addWhere('activity_type_id:name', '=', 'Induction')
->addWhere('status_id:name', '=', 'To be scheduled')
->addWhere('created_date', '<', date('Y-m-d H:i:s', $followUpTimestamp))
->setLimit($batchSize)
->setOffset($offset)
->execute();
// Process each activity in the batch
foreach ($unscheduledInductionActivities as $activity) {
// Check if a follow-up email has already been sent to avoid duplication.
$emailActivities = Activity::get(FALSE)
->addWhere('activity_type_id:name', '=', 'Email')
->addWhere('subject', '=', $template['msg_subject'])
->addWhere('source_contact_id', '=', $activity['source_contact_id'])
->execute()->single();
if (!$emailActivities) {
$emailParams = [
'contact_id' => $activity['source_contact_id'],
'template_id' => $template['id'],
];
civicrm_api3('Email', 'send', $emailParams);
}
}
// Move to the next batch by increasing the offset
$offset += $batchSize;
$iteration++;
} while (count($unscheduledInductionActivities) === $batchSize && $iteration < $maxIterations);
if ($iteration >= $maxIterations) {
\Civi::log()->warning('Maximum iterations reached in sendFollowUpEmails');
}

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.

2 participants