Skip to content

Commit 4d0be15

Browse files
authored
Merge pull request mautic#14952 from andersonjeccel/UXUI-160-a-page-to-see-all-contacts-tagged-as-dnc-or-search-command
[UXUI-160] DNC search command
2 parents f0be028 + 1bf4ed6 commit 4d0be15

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

app/bundles/LeadBundle/Entity/LeadRepository.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,24 @@ protected function addSearchCommandWhereClause($q, $filter): array
940940
);
941941
$returnParameter = true;
942942
break;
943+
case $this->translator->trans('mautic.lead.lead.searchcommand.dnc'):
944+
case $this->translator->trans('mautic.lead.lead.searchcommand.dnc', [], null, 'en_US'):
945+
$anyKeyword = $this->translator->trans('mautic.lead.lead.searchcommand.dnc.any');
946+
$anyKeywordEn = $this->translator->trans('mautic.lead.lead.searchcommand.dnc.any', [], null, 'en_US');
947+
$sq = $this->getEntityManager()->getConnection()->createQueryBuilder();
948+
$sq->select('1')
949+
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
950+
->where($q->expr()->eq('l.id', 'dnc.lead_id'));
951+
952+
if ($string === $anyKeyword || $string === $anyKeywordEn) {
953+
$returnParameter = false;
954+
} else {
955+
$sq->andWhere($q->expr()->eq('dnc.channel', ":$unique"));
956+
$returnParameter = true;
957+
}
958+
$expr = $q->expr()->{$filter->not ? 'notExists' : 'exists'}($sq->getSQL());
959+
$filter->strict = true;
960+
break;
943961
default:
944962
if (in_array($command, $this->availableSearchFields)) {
945963
$expr = $q->expr()->$likeExpr("l.$command", ":$unique");
@@ -1001,6 +1019,7 @@ public function getSearchCommands(): array
10011019
'mautic.lead.lead.searchcommand.sms_sent',
10021020
'mautic.lead.lead.searchcommand.web_sent',
10031021
'mautic.lead.lead.searchcommand.mobile_sent',
1022+
'mautic.lead.lead.searchcommand.dnc',
10041023
];
10051024

10061025
if (!empty($this->availableSearchFields)) {

app/bundles/LeadBundle/Resources/views/Lead/_filter.html.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@
165165
'label': 'mautic.lead.lead.searchcommand.isunowned.label',
166166
'tooltip': 'mautic.lead.lead.searchcommand.isunowned.description',
167167
'icon': 'ri-user-unfollow-line'
168+
},
169+
{
170+
'search': (('mautic.lead.lead.searchcommand.dnc'|trans) ~ ':' ~ ('mautic.lead.lead.searchcommand.dnc.any'|trans)),
171+
'label': 'mautic.lead.lead.searchcommand.dnc.label',
172+
'tooltip': 'mautic.lead.lead.searchcommand.dnc.description',
173+
'icon': 'ri-prohibited-line'
168174
}
169175
]
170176
}) -}}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mautic\LeadBundle\Tests\Functional;
6+
7+
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
8+
use Mautic\LeadBundle\Entity\Lead;
9+
use Symfony\Component\HttpFoundation\Request;
10+
11+
final class DncSearchFunctionalTest extends MauticMysqlTestCase
12+
{
13+
protected $useCleanupRollback = false;
14+
15+
private const MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH = 'Contact with email DNC should appear in dnc:email search';
16+
17+
public function testDncSearchWithAnyChannel(): void
18+
{
19+
$contact1 = $this->createContact('contact1@test.com');
20+
$contact2 = $this->createContact('contact2@test.com');
21+
$contact3 = $this->createContact('contact3@test.com');
22+
23+
$this->addDncRecord($contact1->getId(), 'email');
24+
$this->addDncRecord($contact2->getId(), 'sms');
25+
26+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aany');
27+
$this->assertResponseIsSuccessful();
28+
$responseText = $crawler->text();
29+
30+
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with email DNC should appear in dnc:any search');
31+
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:any search');
32+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:any search');
33+
}
34+
35+
public function testDncSearchWithSpecificChannel(): void
36+
{
37+
$contact1 = $this->createContact('email-dnc@test.com');
38+
$contact2 = $this->createContact('sms-dnc@test.com');
39+
$contact3 = $this->createContact('no-dnc@test.com');
40+
41+
$this->addDncRecord($contact1->getId(), 'email');
42+
$this->addDncRecord($contact2->getId(), 'sms');
43+
44+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aemail');
45+
$this->assertResponseIsSuccessful();
46+
$responseText = $crawler->text();
47+
48+
$this->assertStringContainsString($contact1->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
49+
$this->assertStringNotContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should not appear in dnc:email search');
50+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:email search');
51+
52+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Asms');
53+
$this->assertResponseIsSuccessful();
54+
$responseText = $crawler->text();
55+
56+
$this->assertStringNotContainsString($contact1->getEmail(), $responseText, 'Contact with email DNC should not appear in dnc:sms search');
57+
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:sms search');
58+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:sms search');
59+
}
60+
61+
public function testDncSearchNegation(): void
62+
{
63+
$contact1 = $this->createContact('dnc-contact@test.com');
64+
$contact2 = $this->createContact('normal-contact@test.com');
65+
66+
$this->addDncRecord($contact1->getId(), 'email');
67+
68+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=!dnc%3Aany');
69+
$this->assertResponseIsSuccessful();
70+
$responseText = $crawler->text();
71+
72+
$this->assertStringNotContainsString($contact1->getEmail(), $responseText, 'Contact with DNC should not appear in negative dnc:any search');
73+
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact without DNC should appear in negative dnc:any search');
74+
}
75+
76+
public function testDncSearchWithMultipleChannelsOnSameContact(): void
77+
{
78+
$contact1 = $this->createContact('multi-dnc@test.com');
79+
$contact2 = $this->createContact('single-dnc@test.com');
80+
$contact3 = $this->createContact('no-dnc-multiple@test.com');
81+
82+
$this->addDncRecord($contact1->getId(), 'email');
83+
$this->addDncRecord($contact1->getId(), 'sms');
84+
85+
$this->addDncRecord($contact2->getId(), 'email');
86+
87+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aany');
88+
$this->assertResponseIsSuccessful();
89+
$responseText = $crawler->text();
90+
91+
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with multiple DNC channels should appear in dnc:any search');
92+
$this->assertStringContainsString($contact2->getEmail(), $responseText, 'Contact with single DNC channel should appear in dnc:any search');
93+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without DNC should not appear in dnc:any search');
94+
95+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Aemail');
96+
$this->assertResponseIsSuccessful();
97+
$responseText = $crawler->text();
98+
99+
$this->assertStringContainsString($contact1->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
100+
$this->assertStringContainsString($contact2->getEmail(), $responseText, self::MESSAGE_EMAIL_DNC_SHOULD_APPEAR_IN_EMAIL_SEARCH);
101+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without email DNC should not appear in dnc:email search');
102+
103+
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search=dnc%3Asms');
104+
$this->assertResponseIsSuccessful();
105+
$responseText = $crawler->text();
106+
107+
$this->assertStringContainsString($contact1->getEmail(), $responseText, 'Contact with SMS DNC should appear in dnc:sms search');
108+
$this->assertStringNotContainsString($contact2->getEmail(), $responseText, 'Contact without SMS DNC should not appear in dnc:sms search');
109+
$this->assertStringNotContainsString($contact3->getEmail(), $responseText, 'Contact without SMS DNC should not appear in dnc:sms search');
110+
}
111+
112+
private function createContact(string $email): Lead
113+
{
114+
$contact = new Lead();
115+
$contact->setEmail($email);
116+
$contact->setDateIdentified(new \DateTime());
117+
$this->em->persist($contact);
118+
$this->em->flush();
119+
120+
return $contact;
121+
}
122+
123+
private function addDncRecord(int $contactId, string $channel): void
124+
{
125+
$this->em->getConnection()->executeStatement(
126+
'INSERT INTO '.MAUTIC_TABLE_PREFIX.'lead_donotcontact (lead_id, channel, reason, comments, date_added) VALUES (?, ?, ?, ?, ?)',
127+
[$contactId, $channel, 1, 'Test DNC', new \DateTime()],
128+
[\Doctrine\DBAL\Types\Types::INTEGER, \Doctrine\DBAL\Types\Types::STRING, \Doctrine\DBAL\Types\Types::INTEGER, \Doctrine\DBAL\Types\Types::STRING, \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE]
129+
);
130+
}
131+
}

app/bundles/LeadBundle/Translations/en_US/messages.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ mautic.lead.lead.searchcommand.page_id.description="Filters contacts who have vi
172172
mautic.lead.lead.searchcommand.sms_sent.description="Filters contacts who have been sent a specific SMS"
173173
mautic.lead.lead.searchcommand.web_sent.description="Filters contacts who have been sent a specific web notification"
174174
mautic.lead.lead.searchcommand.mobile_sent.description="Filters contacts who have been sent a specific mobile notification"
175+
mautic.lead.lead.searchcommand.dnc="dnc"
176+
mautic.lead.lead.searchcommand.dnc.any="any"
177+
mautic.lead.lead.searchcommand.dnc.label="Do Not Contact"
178+
mautic.lead.lead.searchcommand.dnc.description="Filters contacts marked as Do Not Contact. Use 'dnc:any' for any channel, or 'dnc:<channel>' for a specific channel (e.g., dnc:email, dnc:sms)."
175179
title.description="Searches contacts by their title"
176180
firstname.description="Searches contacts by their first name"
177181
lastname.description="Searches contacts by their last name"

0 commit comments

Comments
 (0)