Contacts not running through campaign again although "restart the campaign" is set

Your software
My Mautic version is: 2.15.3
My PHP version is: 7.2.22

Your problem
My problem is: Contacts do not run through campaign again, even after it has been removed from segment and campaign and readded.
The Switch “Allow contacts to restart the campaign” is set to yes.

I have a landingpage on my website, where customers are able to select a date and a a discipline they are interested in.

The form puts the contacts in a segment “email.pend”

When the corresponding campaign based on email.pend is triggered it is determined what discipline has been filled out in the form and the contact is put to the corresponding segment. After that it is removed from the current segment email.pend and removed from the campaign also.

This works fine but:
The contact should be able to go to the landing page again to choose a second that with a different discipline and run through the same campaign again, but put in a different segment for the different discipline chosen.

The problem is: The contact never enters the campaign again when it reenters the email.pend segment after filling the webform.

Can anybody tell me how to get the contact through the campaign again?
Or is this a bug related to version 2.15.3?

These errors are showing in the log: not entries in the log

Steps I have tried to fix the problem: made a fresh install of mautic 2.15.3 to be sure the behavior is not related to an update problem.

Thanks everybody for any help :slight_smile:
Best regards

Hi Patrick,

Sorry to hear you’re having problems!

Do you see anything in your logs that might relate to this problem? could it be related to this issue? There’s a PR proposed.

Thank you Ruth

I followed the link but I do not understand how to patch the problem, because I am new to mautic and github anyway :wink:

Can you help me with this?

May be a short step by step process would also be valuable for other users who need to patch something in the future.

What do you think about this?

Thank you very much

Best regards from Switzerland

The patch for #7339 is located in PR#7404.

If you have shell access, here’s how to patch your Mautic with the fix.

  • Download the patch to your temp directory. wget -P /tmp/
  • Make a back up of your Mautic install. cp -var /location/of/mautic/ /location/of/mautic-backup/
  • Change directory to your Mautic install. cd /location/of/mautic/
  • Patch your Mautic install. patch -p1 < /tmp/7704.patch
  • Clear & refresh your cache in Mautic. sudo su - webserveruser -c 'cd /location/of/mautic/; app/console cache:clear' -s /bin/bash

location/of/mautic/ = wherever Mautic is stored on your server
webserveruser = whatever the user your web server runs as


Thank you @MxyzptlkFishStix

What is the way to do this if I only have ftp?

Best regards

Download this file.
Rename it to LeadRepository.php.
Upload it to the /app/bundles/CampaignBundle/Entity/ directory by overwriting the old one.
Delete the /app/cache/prod/ directory and refresh the Mautic dashboard in your browser.


You are great!
TY @MxyzptlkFishStix

Will this patch file work for my version 2.15.3?

Best regards

That’s what I patched it against.

Well then TY for all the help.
I will test this and if it works how can I mark this as solved in the forum?

See if you can edit the title. If so, add [Solved] in front of it. Also, mark the specific post as well.

Hi folks, it’s easier than that … if you tap the three dots at the bottom of the post where the answer is, you’ll find “solution” - on a mobile it’s a check box - if the post is in the Support category.

This marks it as solved and shows the answer in a box on the first post for others coming along later!

I copied the patched LeadRepository.php from @MxyzptlkFishStix to my mautic 2.15.3.
The lead seems to go into the campaign a second time, but then hangs in there pending. Sometimes it hangs on the first entry. See screenshotBildschirmfoto vom 2019-11-26 11-29-11

Is this another bug or does the LeadRepository.php not work?

Here is the patched LeadRepository.php because the file download from @MxyzptlkFishStix is no longer available:

<?php /* * @copyright 2014 Mautic Contributors. All rights reserved * @author Mautic * * @link * * @license GNU/GPLv3 */ namespace Mautic\CampaignBundle\Entity; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\QueryBuilder; use Mautic\CampaignBundle\Entity\Result\CountResult; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; /** * LeadRepository. */ class LeadRepository extends CommonRepository { use ContactLimiterTrait; use SlaveConnectionTrait; /** * Get the details of leads added to a campaign. * * @param $campaignId * @param null $leads * * @return array */ public function getLeadDetails($campaignId, $leads = null) { $q = $this->getEntityManager()->createQueryBuilder() ->from('MauticCampaignBundle:Lead', 'lc') ->select('lc') ->leftJoin('lc.campaign', 'c') ->leftJoin('lc.lead', 'l'); $q->where( $q->expr()->eq('', ':campaign') )->setParameter('campaign', $campaignId); if (!empty($leads)) { $q->andWhere( $q->expr()->in('', ':leads') )->setParameter('leads', $leads); } $results = $q->getQuery()->getArrayResult(); $return = []; foreach ($results as $r) { $return[$r['lead_id']][] = $r; } return $return; } /** * Get leads for a specific campaign. * * @deprecated 2.1.0; Use MauticLeadBundle\Entity\LeadRepository\getEntityContacts() instead * * @param $args * * @return array */ public function getLeadsWithFields($args) { return $this->getEntityManager()->getRepository('MauticLeadBundle:Lead')->getEntityContacts( $args, 'campaign_leads', isset($args['campaign_id']) ? $args['campaign_id'] : 0, ['manually_removed' => 0], 'campaign_id' ); } /** * Get leads for a specific campaign. * * @param $campaignId * @param null $eventId * * @return array */ public function getLeads($campaignId, $eventId = null) { $q = $this->getEntityManager()->createQueryBuilder() ->from('MauticCampaignBundle:Lead', 'lc') ->select('lc, l') ->leftJoin('lc.campaign', 'c') ->leftJoin('lc.lead', 'l'); $q->where( $q->expr()->andX( $q->expr()->eq('lc.manuallyRemoved', ':false'), $q->expr()->eq('', ':campaign') ) ) ->setParameter('false', false, 'boolean') ->setParameter('campaign', $campaignId); if ($eventId != null) { $dq = $this->getEntityManager()->createQueryBuilder(); $dq->select('') ->from('MauticCampaignBundle:LeadEventLog', 'ell') ->leftJoin('ell.lead', 'el') ->leftJoin('ell.event', 'ev') ->where( $dq->expr()->eq('', ':eventId') ); $q->andWhere(' NOT IN('.$dq->getDQL().')') ->setParameter('eventId', $eventId); } $result = $q->getQuery()->getResult(); return $result; } /** * Updates lead ID (e.g. after a lead merge). * * @param $fromLeadId * @param $toLeadId */ public function updateLead($fromLeadId, $toLeadId) { // First check to ensure the $toLead doesn't already exist $results = $this->getEntityManager()->getConnection()->createQueryBuilder() ->select('cl.campaign_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->where('cl.lead_id = '.$toLeadId) ->execute() ->fetchAll(); $campaigns = []; foreach ($results as $r) { $campaigns[] = $r['campaign_id']; } $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $q->update(MAUTIC_TABLE_PREFIX.'campaign_leads') ->set('lead_id', (int) $toLeadId) ->where('lead_id = '.(int) $fromLeadId); if (!empty($campaigns)) { $q->andWhere( $q->expr()->notIn('campaign_id', $campaigns) )->execute(); // Delete remaining leads as the new lead already belongs $this->getEntityManager()->getConnection()->createQueryBuilder() ->delete(MAUTIC_TABLE_PREFIX.'campaign_leads') ->where('lead_id = '.(int) $fromLeadId) ->execute(); } else { $q->execute(); } } /** * Check Lead in campaign. * * @param Lead $lead * @param array $options * * @return bool */ public function checkLeadInCampaigns($lead, $options = []) { if (empty($options['campaigns'])) { return false; } $q = $this->_em->getConnection()->createQueryBuilder(); $q->select('l.campaign_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l'); $q->where( $q->expr()->andX( $q->expr()->eq('l.lead_id', ':leadId'), $q->expr()->in('l.campaign_id', $options['campaigns'], \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) ) ); if (!empty($options['dataAddedLimit'])) { $q->andWhere($q->expr() ->{$options['expr']}('l.date_added', ':dateAdded')) ->setParameter('dateAdded', $options['dateAdded']); } $q->setParameter('leadId', $lead->getId()); return (bool) $q->execute()->fetchColumn(); } /** * @param int $campaignId * @param int $decisionId * @param int $parentDecisionId * @param ContactLimiter $limiter * * @return array */ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, ContactLimiter $limiter) { // Main query $q = $this->getSlaveConnection($limiter)->createQueryBuilder(); $q->select('l.lead_id, l.date_added') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') ->where( $q->expr()->andX( $q->expr()->eq('l.campaign_id', ':campaignId'), $q->expr()->eq('l.manually_removed', 0) ) ) // Order by ID so we can query by greater than X contact ID when batching ->orderBy('l.lead_id') ->setMaxResults($limiter->getBatchLimit()) ->setParameter('campaignId', (int) $campaignId) ->setParameter('decisionId', (int) $decisionId); // Contact IDs $this->updateQueryFromContactLimiter('l', $q, $limiter); // Limit to events that have not been executed or scheduled yet $eventQb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $eventQb->select('null') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') ->where( $eventQb->expr()->andX( $eventQb->expr()->eq('log.event_id', ':decisionId'), $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), $eventQb->expr()->eq('log.rotation', 'l.rotation') ) ); $q->andWhere( sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) ); if ($parentDecisionId) { // Limit to events that have no grandparent or whose grandparent has already been executed $grandparentQb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $grandparentQb->select('null') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'grandparent_log') ->where( $grandparentQb->expr()->eq('grandparent_log.event_id', ':grandparentId'), $grandparentQb->expr()->eq('grandparent_log.lead_id', 'l.lead_id'), $grandparentQb->expr()->eq('grandparent_log.rotation', 'l.rotation') ); $q->setParameter('grandparentId', (int) $parentDecisionId); $q->andWhere( sprintf('EXISTS (%s)', $grandparentQb->getSQL()) ); } if ($limiter->hasCampaignLimit() && $limiter->getCampaignLimitRemaining() < $limiter->getBatchLimit()) { $q->setMaxResults($limiter->getCampaignLimitRemaining()); } $results = $q->execute()->fetchAll(); $contacts = []; foreach ($results as $result) { $contacts[$result['lead_id']] = new \DateTime($result['date_added'], new \DateTimeZone('UTC')); } if ($limiter->hasCampaignLimit()) { $limiter->reduceCampaignLimitRemaining(count($contacts)); } return $contacts; } /** * This is approximate because the query that fetches contacts per decision is based on if the grandparent has been executed or not. * * @param int $decisionId * @param int $parentDecisionId * @param null $specificContactId * * @return int */ public function getInactiveContactCount($campaignId, array $decisionIds, ContactLimiter $limiter) { // We have to loop over each decision to get a count or else any contact that has executed any single one of the decision IDs // will not be included potentially resulting in not having the inactive path analyzed $totalCount = 0; foreach ($decisionIds as $decisionId) { // Main query $q = $this->getSlaveConnection()->createQueryBuilder(); $q->select('count(*)') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') ->where( $q->expr()->andX( $q->expr()->eq('l.campaign_id', ':campaignId'), $q->expr()->eq('l.manually_removed', 0) ) ) // Order by ID so we can query by greater than X contact ID when batching ->orderBy('l.lead_id') ->setParameter('campaignId', (int) $campaignId); // Contact IDs $this->updateQueryFromContactLimiter('l', $q, $limiter, true); // Limit to events that have not been executed or scheduled yet $eventQb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $eventQb->select('null') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') ->where( $eventQb->expr()->andX( $eventQb->expr()->eq('log.event_id', $decisionId), $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), $eventQb->expr()->eq('log.rotation', 'l.rotation') ) ); $q->andWhere( sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) ); $totalCount += (int) $q->execute()->fetchColumn(); } return $totalCount; } /** * @param array $contactIds * @param Campaign $campaign * * @return array */ public function getCampaignMembers(array $contactIds, Campaign $campaign) { $qb = $this->createQueryBuilder('l'); $qb->where( $qb->expr()->andX( $qb->expr()->eq('l.campaign', ':campaign'), $qb->expr()->in('IDENTITY(l.lead)', ':contactIds') ) ) ->setParameter('campaign', $campaign) ->setParameter('contactIds', $contactIds, Connection::PARAM_INT_ARRAY); $results = $qb->getQuery()->getResult(); $campaignMembers = []; /** @var Lead $result */ foreach ($results as $result) { $campaignMembers[$result->getLead()->getId()] = $result; } return $campaignMembers; } /** * @param array $contactIds * @param $campaignId * * @return array */ public function getContactRotations(array $contactIds, $campaignId) { $qb = $this->getEntityManager()->getConnection()->createQueryBuilder(); $qb->select('cl.lead_id, cl.rotation') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->where( $qb->expr()->andX( $qb->expr()->eq('cl.campaign_id', ':campaignId'), $qb->expr()->in('cl.lead_id', ':contactIds') ) ) ->setParameter('campaignId', (int) $campaignId) ->setParameter('contactIds', $contactIds, Connection::PARAM_INT_ARRAY); $results = $qb->execute()->fetchAll(); $contactRotations = []; foreach ($results as $result) { $contactRotations[$result['lead_id']] = $result['rotation']; } return $contactRotations; } /** * @param $campaignId * @param ContactLimiter $limiter * @param bool $campaignCanBeRestarted * * @return CountResult */ public function getCountsForCampaignContactsBySegment($campaignId, ContactLimiter $limiter, $campaignCanBeRestarted = false) { if (!$segments = $this->getCampaignSegments($campaignId)) { return new CountResult(0, 0, 0); } $qb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $qb->select('min(ll.lead_id) as min_id, max(ll.lead_id) as max_id, count(distinct(ll.lead_id)) as the_count') ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll') ->where( $qb->expr()->andX( $qb->expr()->eq('ll.manually_removed', 0), $qb->expr()->in('ll.leadlist_id', $segments) ) ); $this->updateQueryFromContactLimiter('ll', $qb, $limiter, true); $this->updateQueryWithExistingMembershipExclusion($campaignId, $qb, $campaignCanBeRestarted); if (!$campaignCanBeRestarted) { $this->updateQueryWithHistoryExclusion($campaignId, $qb); } $result = $qb->execute()->fetch(); return new CountResult($result['the_count'], $result['min_id'], $result['max_id']); } /** * @param $campaignId * @param ContactLimiter $limiter * @param bool $campaignCanBeRestarted * * @return array */ public function getCampaignContactsBySegments($campaignId, ContactLimiter $limiter, $campaignCanBeRestarted = false) { if (!$segments = $this->getCampaignSegments($campaignId)) { return []; } $qb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $qb->select('distinct(ll.lead_id) as id') ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll') ->where( $qb->expr()->andX( $qb->expr()->eq('ll.manually_removed', 0), $qb->expr()->in('ll.leadlist_id', $segments) ) ); $this->updateQueryFromContactLimiter('ll', $qb, $limiter); $this->updateQueryWithExistingMembershipExclusion($campaignId, $qb, $campaignCanBeRestarted); if (!$campaignCanBeRestarted) { $this->updateQueryWithHistoryExclusion($campaignId, $qb); } $results = $qb->execute()->fetchAll(); $contacts = []; foreach ($results as $result) { $contacts[$result['id']] = $result['id']; } return $contacts; } /** * @param $campaignId * @param ContactLimiter $limiter * * @return int */ public function getCountsForOrphanedContactsBySegments($campaignId, ContactLimiter $limiter) { $segments = $this->getCampaignSegments($campaignId); $qb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $qb->select('min(cl.lead_id) as min_id, max(cl.lead_id) as max_id, count(cl.lead_id) as the_count') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->where( $qb->expr()->andX( $qb->expr()->eq('cl.campaign_id', (int) $campaignId), $qb->expr()->eq('cl.manually_removed', 0), $qb->expr()->eq('cl.manually_added', 0) ) ); $this->updateQueryFromContactLimiter('cl', $qb, $limiter, true); $this->updateQueryWithSegmentMembershipExclusion($segments, $qb); $result = $qb->execute()->fetch(); return new CountResult($result['the_count'], $result['min_id'], $result['max_id']); } /** * @param $campaignId * @param ContactLimiter $limiter * * @return array */ public function getOrphanedContacts($campaignId, ContactLimiter $limiter) { $segments = $this->getCampaignSegments($campaignId); $qb = $this->getSlaveConnection($limiter)->createQueryBuilder(); $qb->select('cl.lead_id as id') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->where( $qb->expr()->andX( $qb->expr()->eq('cl.campaign_id', (int) $campaignId), $qb->expr()->eq('cl.manually_removed', 0), $qb->expr()->eq('cl.manually_added', 0) ) ); $this->updateQueryFromContactLimiter('cl', $qb, $limiter, false); $this->updateQueryWithSegmentMembershipExclusion($segments, $qb); $results = $qb->execute()->fetchAll(); $contacts = []; foreach ($results as $result) { $contacts[$result['id']] = $result['id']; } return $contacts; } /** * Takes an array of contact ID's and increments * their current rotation in a campaign by 1. * * @param array $contactIds * @param int $campaignId * * @return bool */ public function incrementCampaignRotationForContacts(array $contactIds, $campaignId) { $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $q->update(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->set('cl.rotation', 'cl.rotation + 1') ->where( $q->expr()->andX( $q->expr()->in('cl.lead_id', ':contactIds'), $q->expr()->eq('cl.campaign_id', ':campaignId') ) ) ->setParameter('contactIds', $contactIds, Connection::PARAM_INT_ARRAY) ->setParameter('campaignId', (int) $campaignId) ->execute(); } /** * @param $campaignId * * @return array */ private function getCampaignSegments($campaignId) { // Get published segments for this campaign $segmentResults = $this->getEntityManager()->getConnection()->createQueryBuilder() ->select('cl.leadlist_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'cl') ->join('cl', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', ' = cl.leadlist_id and ll.is_published = 1') ->where('cl.campaign_id = '.(int) $campaignId) ->execute() ->fetchAll(); if (empty($segmentResults)) { // No segments so no contacts return []; } $segments = []; foreach ($segmentResults as $result) { $segments[] = $result['leadlist_id']; } return $segments; } /** * @param $campaignId * @param QueryBuilder $qb */ private function updateQueryWithExistingMembershipExclusion($campaignId, QueryBuilder $qb, $campaignCanBeRestarted = false) { $membershipConditions = [ $qb->expr()->eq('cl.lead_id', 'll.lead_id'), $qb->expr()->eq('cl.campaign_id', (int) $campaignId), ]; if ($campaignCanBeRestarted) { $membershipConditions[] = $qb->expr()->eq('cl.manually_removed', 0); } $subq = $this->getEntityManager()->getConnection()->createQueryBuilder() ->select('null') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') ->where( $qb->expr()->andX(...$membershipConditions) ); $qb->andWhere( sprintf('NOT EXISTS (%s)', $subq->getSQL()) ); } /** * @param array $segments * @param QueryBuilder $qb */ private function updateQueryWithSegmentMembershipExclusion(array $segments, QueryBuilder $qb) { if (count($segments) === 0) { // No segments so nothing to exclude return; } $subq = $this->getEntityManager()->getConnection()->createQueryBuilder() ->select('null') ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll') ->where( $qb->expr()->andX( $qb->expr()->eq('ll.lead_id', 'cl.lead_id'), $qb->expr()->eq('ll.manually_removed', 0), $qb->expr()->in('ll.leadlist_id', $segments) ) ); $qb->andWhere( sprintf('NOT EXISTS (%s)', $subq->getSQL()) ); } /** * Exclude contacts with any previous campaign history; this is mainly BC for pre 2.14.0 where the membership entry was deleted. * * @param $campaignId * @param QueryBuilder $qb */ private function updateQueryWithHistoryExclusion($campaignId, QueryBuilder $qb) { $subq = $this->getEntityManager()->getConnection()->createQueryBuilder() ->select('null') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'el') ->where( $qb->expr()->andX( $qb->expr()->eq('el.lead_id', 'll.lead_id'), $qb->expr()->eq('el.campaign_id', (int) $campaignId) ) ); $qb->andWhere( sprintf('NOT EXISTS (%s)', $subq->getSQL()) ); } }


I just tested the PR with a clean Mautic install and tried saw the following:

  • Contact added to segment
  • Contact added to campaign
  • Contact removed from segment
  • Contact removed from campaign
  • Mail successfully sent
  • Contact added to segment (2nd time)
  • Contact added to campaign (2nd time)
  • Contact removed from segment (2nd time)
  • Contact removed from campaign (2nd time)
  • Mail successfully sent (2nd time)
  • Contact added to segment (3rd time)
  • Contact added to campaign (3rd time)
  • Contact removed from segment (3rd time)
  • Contact removed from campaign (3rd time)
  • Mail successfully sent (3rd time)

Did you delete your cache after copying the patched LeadRepository.php? I’m merging this fix now for the 2.16 release, so you can expect it to be in there soon :slight_smile:


As I said I can confirm that it reenters the campaign with the patch, but in my case the lead got stuck in the campaign pending…

And yes of course I deleted the cache :wink:

So this seems to be another bug may be not related with the patch.

I think this surely works better than before with the patch.

Best regards

1 Like