Skip to content

Commit 0edc841

Browse files
Merge pull request #86 from grueneschweiz/api_website_to_mailchimp
[API] New route for directly adding contacts to mailchimp
2 parents 7b41136 + dde5f8a commit 0edc841

7 files changed

Lines changed: 351 additions & 5 deletions

File tree

app/Console/Commands/EndpointCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
abstract class EndpointCommand extends Command
1313
{
1414
public const MC_ENDPOINT_ROUTE_NAME = 'mailchimp_endpoint';
15+
public const WEBSITE_TO_MC_ENDPOINT_ROUTE_NAME = 'website_to_mailchimp_endpoint';
1516

1617
/**
1718
* The config validation errors

app/Http/Controllers/RestApi/RestController.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
use App\MailchimpEndpoint;
66
use App\Synchronizer\MailchimpToCrmSynchronizer;
7+
use App\Synchronizer\WebsiteToMailchimpSynchronizer;
78
use Illuminate\Http\Request;
9+
use Illuminate\Http\JsonResponse;
10+
use App\Exceptions\MailchimpClientException;
11+
use App\Exceptions\EmailComplianceException;
12+
use App\Exceptions\InvalidEmailException;
13+
use App\Exceptions\MemberDeleteException;
14+
use App\Exceptions\ConfigException;
15+
use App\Exceptions\ParseCrmDataException;
816

917
class RestController
1018
{
@@ -35,4 +43,23 @@ public function handleGet(string $secret)
3543
abort(401, 'Invalid secret.');
3644
}
3745
}
46+
47+
/**
48+
* Add a new contact to Mailchimp from the website (using CRM-mapping)
49+
*
50+
* @param Request $request
51+
* @param string $secret
52+
*/
53+
public function addContact(Request $request, string $secret)
54+
{
55+
/** @var MailchimpEndpoint|null $endpoint */
56+
$endpoint = MailchimpEndpoint::where('secret', $secret)->first();
57+
58+
if (!$endpoint) {
59+
abort(401, 'Invalid secret.');
60+
}
61+
62+
$sync = new WebsiteToMailchimpSynchronizer($endpoint->config);
63+
$sync->syncSingle($request->post());
64+
}
3865
}

app/Http/MailChimpClient.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,30 @@ public function putSubscriber(array $mcData, string $email = null, string $id =
293293

294294
return $put;
295295
}
296+
297+
/**
298+
* Add or update tags for a subscriber
299+
*
300+
* @param string $subscriberId The MailChimp subscriber ID
301+
* @param array $tags Array of tag names (strings) or tag objects with 'name' and 'status'
302+
*
303+
* @throws MailchimpClientException
304+
*/
305+
public function addTagsToSubscriber(string $subscriberId, array $tags)
306+
{
307+
if (empty($tags)) {
308+
return;
309+
}
310+
311+
$formattedTags = [];
312+
foreach ($tags as $tag) {
313+
if (is_string($tag)) {
314+
$formattedTags[] = (object)['name' => $tag, 'status' => 'active'];
315+
}
316+
}
317+
318+
$this->postSubscriberTags($subscriberId, ['tags' => $formattedTags]);
319+
}
296320

297321
/**
298322
* Update tags of subscriber

app/Synchronizer/Config.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,24 @@ public function getMailchimpListId(): string
175175
if (empty($this->mailchimp['listId'])) {
176176
throw new ConfigException("Missing mailchimp list id.");
177177
}
178-
178+
179179
return $this->mailchimp['listId'];
180180
}
181-
181+
182+
/**
183+
* Return the tag that should be added to new subscribers
184+
*
185+
* @return string
186+
*/
187+
public function getNewTag(): string
188+
{
189+
if (empty($this->mailchimp['newtag'])) {
190+
return 'new';
191+
}
192+
193+
return $this->mailchimp['newtag'];
194+
}
195+
182196
/**
183197
* The mailchimp merge field key that corresponds to the crm's id
184198
*
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
namespace App\Synchronizer;
4+
5+
use App\Synchronizer\Mapper\Mapper;
6+
use App\Http\MailChimpClient;
7+
use App\Synchronizer\Filter;
8+
9+
class WebsiteToMailchimpSynchronizer
10+
{
11+
/**
12+
* @var Config
13+
*/
14+
private $config;
15+
16+
/**
17+
* @var string
18+
*/
19+
private $configName;
20+
21+
/**
22+
* @var MailChimpClient
23+
*/
24+
private $mailchimpClient;
25+
26+
/**
27+
* @var Filter
28+
*/
29+
private $filter;
30+
31+
/**
32+
* @var Mapper
33+
*/
34+
private $mapper;
35+
36+
/**
37+
* Synchronizer constructor.
38+
*
39+
* @param string $configFileName file name of the config file
40+
*
41+
* @throws \App\Exceptions\ConfigException
42+
* @throws \Exception
43+
*/
44+
public function __construct(string $configFileName)
45+
{
46+
$this->config = new Config($configFileName);
47+
$this->configName = $configFileName;
48+
49+
$mcCred = $this->config->getMailchimpCredentials();
50+
$this->mailchimpClient = new MailChimpClient($mcCred['apikey'], $this->config->getMailchimpListId());
51+
52+
$this->filter = new Filter($this->config->getFieldMaps(), $this->config->getSyncAll());
53+
$this->mapper = new Mapper($this->config->getFieldMaps());
54+
}
55+
56+
/**
57+
* Synchronize a single contact from website data to Mailchimp
58+
*
59+
* @param array $websiteData Data from website in internal format (keys match crmKey from config)
60+
*
61+
* @return array The Mailchimp API response
62+
*
63+
* @throws \App\Exceptions\InvalidEmailException
64+
* @throws \App\Exceptions\EmailComplianceException
65+
* @throws \App\Exceptions\MailchimpClientException
66+
* @throws \App\Exceptions\AlreadyInListException
67+
* @throws \App\Exceptions\FakeEmailException
68+
* @throws \App\Exceptions\UnsubscribedEmailException
69+
* @throws \App\Exceptions\MergeFieldException
70+
* @throws \App\Exceptions\MailchimpTooManySubscriptionsException
71+
* @throws \App\Exceptions\ArchivedException
72+
*/
73+
public function syncSingle(array $websiteData)
74+
{
75+
// Validate that we have an email address
76+
$emailField = $this->getEmailFieldFromConfig();
77+
if (empty($websiteData[$emailField])) {
78+
throw new \App\Exceptions\InvalidEmailException('Email address is required');
79+
}
80+
81+
// Normalize the email address
82+
$websiteData[$emailField] = strtolower(trim($websiteData[$emailField]));
83+
84+
// Fill missing CRM keys with empty values
85+
$websiteData = $this->fillMissingCrmKeys($websiteData);
86+
87+
// Validate email format
88+
if (!filter_var($websiteData[$emailField], FILTER_VALIDATE_EMAIL)) {
89+
throw new \App\Exceptions\InvalidEmailException('Invalid email format: ' . $websiteData[$emailField]);
90+
}
91+
92+
// Map the website data to Mailchimp format
93+
$mailchimpData = $this->mapper->crmToMailchimp($websiteData);
94+
95+
// Ensure we have the email address in the correct format for Mailchimp
96+
$mailchimpData['email_address'] = $websiteData[$emailField];
97+
98+
// Set the status to subscribed for new subscribers
99+
$mailchimpData['status'] = 'subscribed';
100+
101+
// Add the subscriber to Mailchimp
102+
$response = $this->mailchimpClient->putSubscriber($mailchimpData);
103+
104+
// Add tags to new subscriber
105+
$tags = [$this->config->getNewTag()];
106+
if (isset($websiteData['notesCountry'])) {
107+
$tags[] = $websiteData['notesCountry'];
108+
}
109+
$this->mailchimpClient->addTagsToSubscriber($response['id'], $tags);
110+
111+
return $response;
112+
}
113+
114+
/**
115+
* Ensures all required CRM keys are present in the data array, filling missing ones with empty values.
116+
*
117+
* @param array $data
118+
* @return array
119+
*/
120+
private function fillMissingCrmKeys(array $data): array
121+
{
122+
foreach ($this->config->getFieldMaps() as $fieldMap) {
123+
$crmKey = $fieldMap->getCrmKey();
124+
if (!array_key_exists($crmKey, $data)) {
125+
$data[$crmKey] = null;
126+
}
127+
}
128+
return $data;
129+
}
130+
131+
/**
132+
* Get the email field name from the config
133+
*
134+
* @return string The email field name
135+
* @throws \App\Exceptions\ConfigException
136+
*/
137+
private function getEmailFieldFromConfig(): string
138+
{
139+
$fieldMaps = $this->config->getFieldMaps();
140+
141+
foreach ($fieldMaps as $fieldMap) {
142+
if ($fieldMap->isEmail()) {
143+
return $fieldMap->getCrmKey();
144+
}
145+
}
146+
147+
throw new \App\Exceptions\ConfigException('No email field defined in configuration');
148+
}
149+
}

routes/api.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,21 @@
2525
Route::post('webhook/{secret}', function (Request $request, string $secret) {
2626
$controller = new RestController();
2727
$controller->handlePost($request, $secret);
28-
28+
2929
return response('', 204);
3030
})->name(EndpointCommand::MC_ENDPOINT_ROUTE_NAME);
31-
31+
3232
Route::get('webhook/{secret}', function (Request $request, string $secret) {
3333
$controller = new RestController();
3434
$controller->handleGet($secret);
35-
35+
3636
return response('', 204);
3737
})->name(EndpointCommand::MC_ENDPOINT_ROUTE_NAME);
38+
39+
Route::post('contact/{secret}', function (Request $request, string $secret) {
40+
$controller = new RestController();
41+
$controller->addContact($request, $secret);
42+
return response('', 204);
43+
})->name(EndpointCommand::WEBSITE_TO_MC_ENDPOINT_ROUTE_NAME);
3844
});
3945
});

0 commit comments

Comments
 (0)