feat: ✨ handle Mailjet spam complaints via webhook

This commit is contained in:
Corentin Huet 2026-06-04 15:34:29 +02:00
parent 27d0c1fc21
commit b04ca114ae
4 changed files with 93 additions and 25 deletions

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Lib\Helpers\MailjetHelper;
use App\Models\Brand;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
/**
* Undocumented function
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function handleMailjetWebhook(Request $request): \Illuminate\Http\Response
{
$data = $request->all();
if ($data["event"] === "spam") {
$campaign = MailjetHelper::getCampaign($data["mj_campaign_id"]);
if (!$campaign) { return; }
$brand = Brand::query()->where('newsletter_email', $campaign['FromEmail'])->first();
if (!$brand) { return; }
MailjetHelper::unsubscribeContact($brand, $data["email"]);
}
}
}

View file

@ -4,8 +4,6 @@ namespace App\Jobs;
use App\Lib\Helpers\MailjetHelper;
use App\Models\BrandContactList;
use App\Models\CustomerInfo;
use App\Models\SingleOptin;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
@ -22,8 +20,8 @@ class SyncEmailOptOuts implements ShouldQueue
$contacts = MailjetHelper::getContactLists();
foreach ($contacts as $contact) {
$listId = $contact['ID'];
$brandModel = BrandContactList::query()->where('mailjet_list_id', $listId)->with('brand')->first();
$listId = $contact['ID'];
$brandModel = BrandContactList::query()->where('mailjet_list_id', $listId)->with('brand')->first();
if ($brandModel) {
$unsubscribedContacts = MailjetHelper::getUnsubscribedContacts($listId, Carbon::now()->subDay());
@ -33,27 +31,9 @@ class SyncEmailOptOuts implements ShouldQueue
}
foreach ($unsubscribedContacts as $unsubscribedContact) {
$contact = MailjetHelper::getContact($unsubscribedContact['ContactID']);
$crmCustomer = CustomerInfo::getFromEmail($brandModel->brand, $contact[0]["Email"])->first();
$contact = MailjetHelper::getContact($unsubscribedContact['ContactID']);
if ($crmCustomer) {
$crmCustomer->opt_in = false;
if (!$crmCustomer->saveOrFail()) {
throw new \Exception(sprintf('Optin status not changed for customer (%s) and brand (%d).', $crmCustomer->hashcode, $brandModel->brand->name));
}
} else {
$singleOptin = SingleOptin::query()->where('email', $contact[0]['Email'])
->whereHas('brand', function ($q) use ($brandModel) {
$q->whereId($brandModel->brand->getKey());
})->first();
if ($singleOptin) {
if (!$singleOptin->delete()) {
throw new \Exception(sprintf('SingleOptin (%s) not deleted for brand (%d).', $singleOptin->hashcode, $brandModel->brand->name));
}
}
}
MailjetHelper::unsubscribeContact($brandModel->brand, $contact[0]["Email"]);
}
}
}

View file

@ -2,10 +2,32 @@
namespace App\Lib\Helpers;
use App\Models\Brand;
use App\Models\CustomerInfo;
use App\Models\SingleOptin;
use Mailjet\Resources;
class MailjetHelper
{
/**
* Undocumented function
*
* @param integer $campaignId
* @return array
*/
public static function getCampaign(int $campaignId): array
{
$response = app('mailjetClient')->get(Resources::$Campaign, [
'id' => $campaignId
]);
if ($response->success()) {
return $response->getData();
} else {
return [];
}
}
/**
* Retrieves contact lists
*
@ -106,7 +128,7 @@ class MailjetHelper
/**
* Uploads a CSV file
*
*
* @param integer $mailjetListId
* @param string $csvFile
* @return array
@ -148,4 +170,35 @@ class MailjetHelper
return [];
}
}
/**
* Undocumented function
*
* @param \App\Models\Brand $brandModel
* @param string $email
* @return void
*/
public static function unsubscribeContact(Brand $brandModel, string $email): void
{
$crmCustomer = CustomerInfo::getFromEmail($brandModel, $email)->first();
if ($crmCustomer) {
$crmCustomer->opt_in = false;
if (!$crmCustomer->saveOrFail()) {
throw new \Exception(sprintf('Optin status not changed for customer (%s) and brand (%d).', $crmCustomer->hashcode, $brandModel->name));
}
} else {
$singleOptin = SingleOptin::query()->where('email', $email)
->whereHas('brand', function ($q) use ($brandModel) {
$q->whereId($brandModel->getKey());
})->first();
if ($singleOptin) {
if (!$singleOptin->delete()) {
throw new \Exception(sprintf('SingleOptin (%s) not deleted for brand (%d).', $singleOptin->hashcode, $brandModel->name));
}
}
}
}
}

View file

@ -15,6 +15,7 @@ use App\Http\Controllers\Api\V4\CustomerController as V4CustomerController;
use App\Http\Controllers\Api\V4\OptinController as V4OptinController;
use App\Http\Controllers\Api\V4\QuestionController as V4QuestionController;
use App\Http\Controllers\Api\V4\StatsController as V4StatsController;
use App\Http\Controllers\Api\WebhookController;
use Illuminate\Support\Facades\Route;
/*
@ -140,3 +141,5 @@ Route::prefix('v4')->name('apiV4.')->group(function () {
Route::post('/claim', [V4ClaimController::class, 'store'])->name('claim.store');
});
});
Route::post('/webhook/mailjet', [WebhookController::class, 'handleMailjetWebhook'])->name('webhook.mailjet');