diff --git a/wp-content/civi-extensions/goonjcustom/CRM/Goonjcustom/Engine.php b/wp-content/civi-extensions/goonjcustom/CRM/Goonjcustom/Engine.php index 49af9787fe..4e15671f16 100644 --- a/wp-content/civi-extensions/goonjcustom/CRM/Goonjcustom/Engine.php +++ b/wp-content/civi-extensions/goonjcustom/CRM/Goonjcustom/Engine.php @@ -10,8 +10,11 @@ class CRM_Goonjcustom_Engine { /** * */ - public static function processQueue($params) { - $returnValues = []; + public static function processQueue(int $maxSeconds = 60): array { + $returnValues = [ + 'processed' => 0, + 'results' => [], + ]; $queue = \CRM_Queue_Service::singleton()->create([ 'name' => self::QUEUE_NAME, @@ -24,7 +27,8 @@ public static function processQueue($params) { 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, ]); - $maxRunTime = time() + 30; + $maxSeconds = max(1, (int) $maxSeconds); + $maxRunTime = time() + $maxSeconds; $continue = TRUE; // Loop to process the queue items. @@ -35,10 +39,18 @@ public static function processQueue($params) { $continue = FALSE; } - $returnValues[] = $result; + if (isset($result['exception']) && $result['exception'] instanceof \Throwable) { + $result['exception'] = [ + 'type' => get_class($result['exception']), + 'message' => $result['exception']->getMessage(), + ]; + } + + $returnValues['results'][] = $result; + $returnValues['processed']++; } - return civicrm_api3_create_success($returnValues, $params, 'SendAuthorizationQueue', 'Run'); + return $returnValues; } } diff --git a/wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php b/wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php index 4c5aa40ba4..6dd4668a07 100644 --- a/wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php +++ b/wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php @@ -745,8 +745,9 @@ public static function aclCollectionCamp($entity, &$clauses, $userId, $condition ->addWhere('status', '=', 'Added') ->addWhere('group_id.Chapter_Contact_Group.Use_Case', '=', 'chapter-team') ->execute(); - $teamGroupContact = $teamGroupContacts->first(); + error_log("teamGroupContact: " . print_r($teamGroupContact, TRUE)); + if (!$teamGroupContact) { // @todo we should handle it in a better way. @@ -763,8 +764,13 @@ public static function aclCollectionCamp($entity, &$clauses, $userId, $condition ->addWhere('id', '=', $groupId) ->execute(); + $group = $chapterGroups->first(); + error_log("group: " . print_r($group, TRUE)); + $statesControlled = $group['Chapter_Contact_Group.States_controlled']; + error_log("statesControlled: " . print_r($statesControlled, TRUE)); + if (empty($statesControlled)) { // Handle the case when the group is not controlling any state. diff --git a/wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php b/wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php index 081d6a1e0a..4361897e28 100644 --- a/wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php +++ b/wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php @@ -1742,4 +1742,193 @@ public static function generateInvoiceNumber(string $op, string $objectName, int } } + /** + * Send camp outcome acknowledgement email after 5 days. + */ + public static function sendCampOutcomeAckEmailAfter5Days($collectionCamp) { + $campId = $collectionCamp['id']; + + // Fetch Event Volunteer + $volunteeringActivities = Activity::get(FALSE) + ->addSelect('activity_contact.contact_id') + ->addJoin('ActivityContact AS activity_contact', 'LEFT') + ->addWhere('activity_type_id:name', '=', 'Volunteering') + ->addWhere('Volunteering_Activity.Collection_Camp', '=', $campId) + ->addWhere('activity_contact.record_type_id', '=', 3) + ->execute(); + + // Collect volunteer contact IDs + $volunteerContactIds = []; + foreach ($volunteeringActivities as $volunteer) { + $volunteerContactIds[] = $volunteer['activity_contact.contact_id']; + } + + // Fetch volunteer emails (if any volunteers found) + $eventVolunteerEmails = []; + if (!empty($volunteerContactIds)) { + $volunteerContacts = Contact::get(FALSE) + ->addSelect('email.email') + ->addJoin('Email AS email', 'LEFT') + ->addWhere('id', 'IN', $volunteerContactIds) + ->execute(); + + foreach ($volunteerContacts as $contact) { + if (!empty($contact['email.email'])) { + $eventVolunteerEmails[] = $contact['email.email']; + } + } + } + + // Convert to comma-separated string for CC + $eventVolunteerCC = implode(',', $eventVolunteerEmails); + + $collectionCamps = EckEntity::get('Collection_Camp', FALSE) + ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp', 'Core_Contribution_Details.Number_of_unique_contributors', 'Camp_Outcome.Rate_the_camp', 'Camp_Outcome.Total_Fundraised_form_Activity', 'Collection_Camp_Intent_Details.Start_Date', 'title', 'Collection_Camp_Intent_Details.End_Date') + ->addWhere('id', '=', $campId) + ->execute()->single(); + + $contribution = Contribution::get(FALSE) + ->addSelect('total_amount') + ->addWhere('contribution_status_id:name', '=', 'Completed') + ->addWhere('Contribution_Details.Source', '=', $campId) + ->execute(); + + $totalAmount = 0; + + foreach ($contribution as $c) { + $totalAmount += $c['total_amount']; + } + + $collectionSourceVehicleDispatche = EckEntity::get('Collection_Source_Vehicle_Dispatch', FALSE) + ->addSelect('Acknowledgement_For_Logistics.No_of_bags_received_at_PU_Office') + ->addWhere('Camp_Vehicle_Dispatch.Collection_Camp', '=', $campId) + ->execute(); + + $materialGeneratedList = []; + foreach ($collectionSourceVehicleDispatche as $dispatch) { + $materialGeneratedList[] = $dispatch['Acknowledgement_For_Logistics.No_of_bags_received_at_PU_Office']; + } + + $materialGeneratedHtml = ''; + if (!empty($collectionSourceVehicleDispatche)) { + // Outer bullet + $materialGeneratedHtml .= "
  • Material generated:
    "; + // Inner numbered list with inline style for emails + $materialGeneratedHtml .= "
      "; + foreach ($collectionSourceVehicleDispatche as $dispatch) { + $materialGeneratedHtml .= "
    1. " . $dispatch['Acknowledgement_For_Logistics.No_of_bags_received_at_PU_Office'] . "
    2. "; + } + $materialGeneratedHtml .= "
  • "; + } + + $uniqueContributors = $collectionCamps['Core_Contribution_Details.Number_of_unique_contributors']; + + $campRating = $collectionCamps['Camp_Outcome.Rate_the_camp']; + $fundsGenerated = $collectionCamps['Camp_Outcome.Total_Fundraised_form_Activity']; + $collectionCampTitle = $collectionCamps['title']; + + $campAddress = $collectionCamps['Collection_Camp_Intent_Details.Location_Area_of_camp']; + + $campStartDate = $collectionCamps['Collection_Camp_Intent_Details.Start_Date']; + $campEndDate = $collectionCamps['Collection_Camp_Intent_Details.End_Date']; + + $campCompletionDate = $collectionCamp['Camp_Outcome.Camp_Status_Completion_Date']; + $campOrganiserId = $collectionCamp['Collection_Camp_Core_Details.Contact_Id']; + + $campAttendedBy = Contact::get(FALSE) + ->addSelect('email.email', 'display_name') + ->addJoin('Email AS email', 'LEFT') + ->addWhere('id', '=', $campOrganiserId) + ->execute()->first(); + + $attendeeEmail = $campAttendedBy['email.email']; + $attendeeName = $campAttendedBy['display_name']; + + if (!$attendeeEmail) { + throw new \Exception('Attendee email missing'); + } + + $mailParams = [ + 'subject' => $attendeeName . ' thankyou for organizing the camp! A quick snapshot.', + 'from' => self::getFromAddress(), + 'toEmail' => $attendeeEmail, + 'replyTo' => self::getFromAddress(), + 'html' => self::getCampOutcomeAckEmailAfter5Days($attendeeName, $campAddress, $campStartDate, $totalAmount, $materialGeneratedHtml, $uniqueContributors, $campRating, $fundsGenerated, $campId, $campEndDate, $campOrganiserId), + 'cc' => $eventVolunteerCC, + ]; + + $emailSendResult = \CRM_Utils_Mail::send($mailParams); + + if ($emailSendResult) { + \Civi::log()->info("Camp status email sent for collection camp: $campId"); + try { + EckEntity::update('Collection_Camp', FALSE) + ->addValue('Camp_Outcome.Five_Day_Email_Sent', 1) + ->addWhere('id', '=', $campId) + ->execute(); + + } + catch (\CiviCRM_API4_Exception $ex) { + \Civi::log()->debug("Exception while creating update the email sent " . $ex->getMessage()); + } + try { + $results = Activity::create(FALSE) + ->addValue('subject', $collectionCampTitle) + ->addValue('activity_type_id:name', 'Camp summary email') + ->addValue('status_id:name', 'Authorized') + ->addValue('activity_date_time', date('Y-m-d H:i:s')) + ->addValue('source_contact_id', $campOrganiserId) + ->addValue('target_contact_id', $campOrganiserId) + ->addValue('Collection_Camp_Data.Collection_Camp_ID', $campId) + ->execute(); + + } + catch (\CiviCRM_API4_Exception $ex) { + \Civi::log()->debug("Exception while creating Camp summary email activity: " . $ex->getMessage()); + } + + } + + } + + /** + * + */ +public static function getCampOutcomeAckEmailAfter5Days($attendeeName, $campAddress, $campStartDate, $totalAmount, $materialGeneratedHtml, $uniqueContributors, $campRating, $fundsGenerated, $campId, $campEndDate, $campOrganiserId) { + $homeUrl = \CRM_Utils_System::baseCMSURL(); + $campVolunteerFeedback = $homeUrl . 'volunteer-camp-feedback/#?Collection_Source_Feedback.Collection_Camp_Code=' . $campId . '&Collection_Source_Feedback.Collection_Camp_Address=' . urlencode($campAddress) . '&Collection_Source_Feedback.Filled_By=' . $campOrganiserId; + // Conditionally include funds raised + $fundsGeneratedHtml = ''; + if (!empty($fundsGenerated)) { + $fundsGeneratedHtml = "
  • Funds raised through activities: $fundsGenerated
  • "; + } + + $formattedCampStartDate = date('d-m-Y', strtotime($campStartDate)); + $formattedCampEndDate = date('d-m-Y', strtotime($campEndDate)); + + // Conditional date text + if ($formattedCampStartDate === $formattedCampEndDate) { + $campDateText = "on $formattedCampStartDate"; + } else { + $campDateText = "from $formattedCampStartDate to $formattedCampEndDate"; + } + + $html = " +

    Dear $attendeeName,

    +

    Thank you for organising the recent collection drive at $campAddress $campDateText! Your effort brought people together and added strength to this movement of mindful giving.

    +

    Here’s a quick snapshot of the camp:

    + +

    If you haven’t filled the feedback form yet, you can share your thoughts here: Feedback Form

    +

    We would also love to hear about any highlights, challenges, or ideas you’d like us to know. Your reflections will help us make future drives even more impactful.

    +

    Looking forward to many more such collaborations ahead!

    +

    Warm regards,
    Team Goonj

    + "; + + return $html; + } + } diff --git a/wp-content/civi-extensions/goonjcustom/api/v3/GoonjcustomAction.php b/wp-content/civi-extensions/goonjcustom/api/v3/GoonjcustomAction.php index 44b1fb3419..d86126b02d 100644 --- a/wp-content/civi-extensions/goonjcustom/api/v3/GoonjcustomAction.php +++ b/wp-content/civi-extensions/goonjcustom/api/v3/GoonjcustomAction.php @@ -18,7 +18,17 @@ * * @throws \CRM_Core_Exception */ +function _civicrm_api3_goonjcustom_action_process_queue_spec(&$spec) { + $spec['max_seconds'] = [ + 'title' => 'Max Seconds', + 'description' => 'Maximum number of seconds to process queue items.', + 'type' => CRM_Utils_Type::T_INT, + 'api.default' => 60, + ]; +} + function civicrm_api3_goonjcustom_action_process_queue($params) { - $returnValues = CRM_Goonjcustom_Engine::processQueue(60); + $maxSeconds = isset($params['max_seconds']) ? (int) $params['max_seconds'] : 60; + $returnValues = CRM_Goonjcustom_Engine::processQueue($maxSeconds); return civicrm_api3_create_success($returnValues, $params, 'GoonjcustomQueue', 'Process'); } diff --git a/wp-content/civi-extensions/goonjcustom/cli/assign_contacts_to_group.php b/wp-content/civi-extensions/goonjcustom/cli/assign_contacts_to_group.php new file mode 100644 index 0000000000..17fe0d144a --- /dev/null +++ b/wp-content/civi-extensions/goonjcustom/cli/assign_contacts_to_group.php @@ -0,0 +1,155 @@ + null, + 'source' => null, + 'email_domain' => null, + 'status' => 'Added', + 'dry_run' => false, + 'help' => false, + ]; + + foreach ($argv as $arg) { + if ($arg === '--help' || $arg === '-h') { + $args['help'] = true; + continue; + } + if ($arg === '--dry-run') { + $args['dry_run'] = true; + continue; + } + if (!str_starts_with($arg, '--') || !str_contains($arg, '=')) { + continue; + } + + [$key, $value] = explode('=', substr($arg, 2), 2); + switch ($key) { + case 'group-id': + $args['group_id'] = (int) $value; + break; + case 'source': + $args['source'] = (string) $value; + break; + case 'email-domain': + $args['email_domain'] = (string) $value; + break; + case 'status': + $args['status'] = (string) $value; + break; + } + } + + return $args; +} + +function main(array $opts): void { + requireCiviBootstrap(); + + if (empty($opts['group_id'])) { + fwrite(STDERR, "Missing required --group-id.\n\n"); + usage(); + exit(2); + } + + $status = $opts['status'] ?: 'Added'; + if (!in_array($status, ['Added', 'Removed', 'Pending'], true)) { + fwrite(STDERR, "Invalid --status. Use Added, Removed, or Pending.\n"); + exit(2); + } + + $where = []; + $params = [ + 1 => [(int) $opts['group_id'], 'Integer'], + 2 => [$status, 'String'], + ]; + + $from = 'civicrm_contact c'; + + if (!empty($opts['email_domain'])) { + $from .= ' INNER JOIN civicrm_email e ON e.contact_id = c.id'; + $where[] = 'e.email LIKE %3'; + $params[3] = ['%@' . $opts['email_domain'], 'String']; + } + + if (!empty($opts['source'])) { + $where[] = 'c.source = %4'; + $params[4] = [$opts['source'], 'String']; + } + + if (empty($where)) { + fwrite(STDERR, "Refusing to run without a filter. Provide --source and/or --email-domain.\n"); + exit(2); + } + + $whereSql = implode(' AND ', $where); + + // Use ON DUPLICATE KEY UPDATE so existing memberships get re-added. + $sql = " + INSERT INTO civicrm_group_contact (group_id, contact_id, status) + SELECT %1 AS group_id, c.id AS contact_id, %2 AS status + FROM {$from} + WHERE {$whereSql} + ON DUPLICATE KEY UPDATE status = VALUES(status) + "; + + $sql = preg_replace('/\\s+/', ' ', trim($sql)); + + if (!empty($opts['dry_run'])) { + fwrite(STDOUT, $sql . "\n"); + exit(0); + } + + $dao = \CRM_Core_DAO::executeQuery($sql, $params); + $affected = method_exists($dao, 'affectedRows') ? (int) $dao->affectedRows() : 0; + + fwrite(STDOUT, "Done. SQL affected rows: {$affected}\n"); + fwrite(STDOUT, "If this is a Smart Group, run the 'Update Smart Groups Cache' scheduled job.\n"); +} + +$opts = parseArgs(array_slice($argv, 1)); +if (!empty($opts['help'])) { + usage(); + exit(0); +} + +main($opts); + diff --git a/wp-content/civi-extensions/goonjcustom/cli/seed_fake_contacts.php b/wp-content/civi-extensions/goonjcustom/cli/seed_fake_contacts.php new file mode 100644 index 0000000000..05d7b59961 --- /dev/null +++ b/wp-content/civi-extensions/goonjcustom/cli/seed_fake_contacts.php @@ -0,0 +1,194 @@ + 1000, + 'start' => 1, + 'batch' => 500, + 'email_domain' => 'example.invalid', + 'group_id' => null, + 'source' => 'Fake Load Test', + 'dry_run' => false, + 'help' => false, + ]; + + foreach ($argv as $arg) { + if ($arg === '--help' || $arg === '-h') { + $args['help'] = true; + continue; + } + if ($arg === '--dry-run') { + $args['dry_run'] = true; + continue; + } + + if (!str_starts_with($arg, '--') || !str_contains($arg, '=')) { + continue; + } + [$key, $value] = explode('=', substr($arg, 2), 2); + + switch ($key) { + case 'count': + case 'start': + case 'batch': + $args[$key] = (int) $value; + break; + + case 'email-domain': + $args['email_domain'] = (string) $value; + break; + + case 'group-id': + $args['group_id'] = $value === '' ? null : (int) $value; + break; + + case 'source': + $args['source'] = (string) $value; + break; + } + } + + $args['count'] = max(0, (int) $args['count']); + $args['start'] = max(1, (int) $args['start']); + $args['batch'] = max(1, (int) $args['batch']); + + return $args; +} + +function requireCiviBootstrap(): void { + if (!class_exists(\Civi::class) || !class_exists(Contact::class) || !function_exists('civicrm_api3')) { + fwrite(STDERR, "CiviCRM not bootstrapped. Run this via `cv php:script ...` (see --help).\n"); + exit(2); + } +} + +function makeEmail(int $i, string $domain): string { + // Keep it deterministic and unique. + return "fake{$i}@{$domain}"; +} + +function seedFakeContacts(array $opts): void { + requireCiviBootstrap(); + + $count = $opts['count']; + $start = $opts['start']; + $batchSize = $opts['batch']; + $emailDomain = $opts['email_domain']; + $groupId = $opts['group_id']; + $source = $opts['source']; + $dryRun = $opts['dry_run']; + + fwrite(STDOUT, "Seeding {$count} contacts (start={$start}, batch={$batchSize}, domain={$emailDomain})\n"); + if ($groupId) { + fwrite(STDOUT, "Adding to group_id={$groupId}\n"); + } + if ($dryRun) { + fwrite(STDOUT, "Dry-run enabled (no writes).\n"); + } + + $createdTotal = 0; + $batchStart = $start; + $lastIndex = $start + $count - 1; + + while ($batchStart <= $lastIndex) { + $batchEnd = min($lastIndex, $batchStart + $batchSize - 1); + $records = []; + + for ($i = $batchStart; $i <= $batchEnd; $i++) { + $firstName = "Fake{$i}"; + $lastName = "User"; + $records[] = [ + 'contact_type' => 'Individual', + 'first_name' => $firstName, + 'last_name' => $lastName, + 'display_name' => "{$firstName} {$lastName}", + 'source' => $source, + 'preferred_language' => 'en_US', + // Ensure they are emailable unless you explicitly want opt-outs. + 'do_not_email' => 0, + 'is_opt_out' => 0, + ]; + } + + if ($dryRun) { + $created = array_map(fn($r) => $r['display_name'], $records); + fwrite(STDOUT, "Would create " . count($created) . " contacts: {$created[0]} ... " . end($created) . "\n"); + $createdTotal += count($records); + $batchStart = $batchEnd + 1; + continue; + } + + $contactIds = []; + foreach ($records as $offset => $record) { + $i = $batchStart + $offset; + + $contact = civicrm_api3('Contact', 'create', $record); + if (!empty($contact['id'])) { + $contactId = (int) $contact['id']; + $contactIds[] = $contactId; + + civicrm_api3('Email', 'create', [ + 'contact_id' => $contactId, + 'email' => makeEmail($i, $emailDomain), + 'is_primary' => 1, + // If "Home" doesn't exist, CiviCRM will pick default; keeping numeric avoids API4 syntax. + // 'location_type_id' => 1, + ]); + + if ($groupId) { + civicrm_api3('GroupContact', 'create', [ + 'group_id' => (int) $groupId, + 'contact_id' => $contactId, + 'status' => 'Added', + ]); + } + } + } + + $createdTotal += count($contactIds); + fwrite(STDOUT, "Created {$createdTotal}/{$count} (last batch: {$batchStart}-{$batchEnd})\n"); + + $batchStart = $batchEnd + 1; + } + + fwrite(STDOUT, "Done. Total created: {$createdTotal}\n"); +} + +$opts = parseArgs(array_slice($argv, 1)); +if ($opts['help']) { + usage(); + exit(0); +} + +seedFakeContacts($opts); diff --git a/wp-content/civi-extensions/uk.co.vedaconsulting.mosaico/CRM/Mosaico/UrlFilter.php b/wp-content/civi-extensions/uk.co.vedaconsulting.mosaico/CRM/Mosaico/UrlFilter.php index 8733e86dd1..8c527cea38 100644 --- a/wp-content/civi-extensions/uk.co.vedaconsulting.mosaico/CRM/Mosaico/UrlFilter.php +++ b/wp-content/civi-extensions/uk.co.vedaconsulting.mosaico/CRM/Mosaico/UrlFilter.php @@ -50,12 +50,25 @@ public function filterHtml($htmls) { } }; - $htmls = preg_replace_callback(';(\]*src *= *")([^">]+)(");i', $callback, $htmls); - $htmls = preg_replace_callback(';(\]*src *= *\')([^">]+)(\');i', $callback, $htmls); - $htmls = preg_replace_callback(';(\]*background *= *")([^">]+)(");i', $callback, $htmls); - $htmls = preg_replace_callback(';(\
    ]*background *= *")([^\'>]+)(\');i', $callback, $htmls); - // WISHLIST: CSS backgrounds? - return $htmls; + $filterOne = function ($html) use ($callback) { + $html = preg_replace_callback(';(\]*src *= *")([^">]+)(");i', $callback, $html); + $html = preg_replace_callback(';(\]*src *= *\')([^">]+)(\');i', $callback, $html); + $html = preg_replace_callback(';(\
    ]*background *= *")([^">]+)(");i', $callback, $html); + $html = preg_replace_callback(';(\
    ]*background *= *\')([^">]+)(\');i', $callback, $html); + // WISHLIST: CSS backgrounds? + return $html; + }; + + // preg_replace_callback() supports arrays, but processing a large batch as a single array + // may spike memory usage. Process items one-by-one to reduce peak memory. + if (is_array($htmls)) { + foreach ($htmls as $k => $v) { + $htmls[$k] = $filterOne($v); + } + return $htmls; + } + + return $filterOne($htmls); } /**