Compare commits
10 commits
master
...
57-sync-op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cec2ab5343 | ||
|
|
174171bd34 | ||
|
|
45ab798b59 | ||
|
|
8170bb395a | ||
|
|
b04ca114ae | ||
|
|
27d0c1fc21 | ||
|
|
d0828ae213 | ||
|
|
b30af29d0f | ||
|
|
6fabb40ab6 | ||
|
|
98ef3c826e |
25 changed files with 1010 additions and 96 deletions
|
|
@ -49,3 +49,7 @@ SAML2_SP_CERT_x509=
|
||||||
SAML2_SP_CERT_PRIVATEKEY=
|
SAML2_SP_CERT_PRIVATEKEY=
|
||||||
SAML2_CONTACT_TECHNICAL_EMAIL=
|
SAML2_CONTACT_TECHNICAL_EMAIL=
|
||||||
SAML2_CONTACT_SUPPORT_EMAIL=
|
SAML2_CONTACT_SUPPORT_EMAIL=
|
||||||
|
|
||||||
|
# MJ - API
|
||||||
|
MJ_APIKEY_PUBLIC=
|
||||||
|
MJ_APIKEY_PRIVATE=
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class Kernel extends ConsoleKernel
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
$schedule->job(StartStatJobs::class)->dailyAt('03:30');
|
$schedule->job(StartStatJobs::class)->dailyAt('03:30');
|
||||||
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
|
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
|
||||||
|
$schedule->job(SyncEmailOptOuts::class)->dailyAt('00:00');
|
||||||
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
|
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
app/Exceptions/MailjetException.php
Normal file
32
app/Exceptions/MailjetException.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Mailjet\Response;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class MailjetException extends RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Error message returned by the Mailjet API
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $returnedMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Mailjet exception
|
||||||
|
*
|
||||||
|
* @param \Mailjet\Response $response
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
public function __construct(Response $response, string $message)
|
||||||
|
{
|
||||||
|
$this->returnedMessage = $response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase();
|
||||||
|
|
||||||
|
parent::__construct(
|
||||||
|
$message,
|
||||||
|
$response->getStatus()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Http/Controllers/Api/WebhookController.php
Normal file
46
app/Http/Controllers/Api/WebhookController.php
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Lib\Helpers\MailjetHelper;
|
||||||
|
use App\Models\Brand;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Exceptions\MailjetException;
|
||||||
|
|
||||||
|
class WebhookController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handles incoming Mailjet webhook events
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function handleMailjetWebhook(Request $request): \Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->all();
|
||||||
|
|
||||||
|
if ($data["event"] === "spam") {
|
||||||
|
try {
|
||||||
|
$campaign = MailjetHelper::getCampaign($data['mj_campaign_id']);
|
||||||
|
|
||||||
|
if (!$campaign) {
|
||||||
|
return response()->json(['status' => 'error', 'message' => 'Campaign not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$brand = Brand::getBrandByNewsletterEmail($campaign[0]['FromEmail']);
|
||||||
|
if (!$brand) {
|
||||||
|
return response()->json(['status' => 'error', 'message' => 'Brand not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
MailjetHelper::unsubscribeContact($brand, $data['email']);
|
||||||
|
|
||||||
|
return response()->json(['status' => 'ok']);
|
||||||
|
} catch (MailjetException $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
return response()->json(['status' => 'error', 'message' => $e->returnedMessage]);
|
||||||
|
}
|
||||||
|
}//end if
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ use App\Models\CustomerInfo;
|
||||||
use App\Models\CustomerLogin;
|
use App\Models\CustomerLogin;
|
||||||
use App\Models\Question;
|
use App\Models\Question;
|
||||||
use App\Models\SingleOptin;
|
use App\Models\SingleOptin;
|
||||||
|
use App\Models\BrandContactList;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Query\JoinClause;
|
use Illuminate\Database\Query\JoinClause;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -23,6 +24,8 @@ use Illuminate\Support\Facades\Session;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use League\Csv\Writer;
|
use League\Csv\Writer;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use App\Lib\Helpers\MailjetHelper;
|
||||||
|
use App\Exceptions\MailjetException;
|
||||||
|
|
||||||
class CustomerController extends Controller
|
class CustomerController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -168,34 +171,19 @@ class CustomerController extends Controller
|
||||||
* Exports customers following the requested export type.
|
* Exports customers following the requested export type.
|
||||||
*
|
*
|
||||||
* @param \App\Http\Requests\Bo\CustomerExport $request
|
* @param \App\Http\Requests\Bo\CustomerExport $request
|
||||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
* @return \Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse
|
||||||
* @throws \Exception If the request export type in not handled.
|
* @throws \Exception If the request export type in not handled.
|
||||||
*/
|
*/
|
||||||
public function exportSubmit(CustomerExport $request): \Symfony\Component\HttpFoundation\StreamedResponse
|
public function exportSubmit(
|
||||||
{
|
CustomerExport $request
|
||||||
|
): \Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse {
|
||||||
$pagination = 2000;
|
$pagination = 2000;
|
||||||
$header = '"lastname";"firstname";"email";"hashcode"';
|
|
||||||
|
|
||||||
$w = Writer::createFromString($header . PHP_EOL);
|
$w = $this->buildCsvWriter($request->action);
|
||||||
$w->setDelimiter(';');
|
|
||||||
$w->setEnclosure('"');
|
|
||||||
$w->setOutputBOM(Writer::BOM_UTF8);
|
|
||||||
|
|
||||||
$exportType = CustomerExportType::make(\intval($request->export_type));
|
$exportType = CustomerExportType::make(\intval($request->export_type));
|
||||||
|
|
||||||
// * We send the CSV using Symfony StreamedResponse.
|
$response = $this->buildStreamedResponse($exportType);
|
||||||
$response = new StreamedResponse();
|
|
||||||
$response->headers->set('Content-Encoding', 'none');
|
|
||||||
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
|
|
||||||
$response->headers->set('Description', 'File Transfer');
|
|
||||||
$response->headers->set(
|
|
||||||
'Content-Disposition',
|
|
||||||
sprintf(
|
|
||||||
'attachment; filename="Export pour newsletter %s %s.csv"',
|
|
||||||
Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'),
|
|
||||||
Str::slug($exportType->label())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
switch ($exportType) {
|
switch ($exportType) {
|
||||||
case CustomerExportType::both:
|
case CustomerExportType::both:
|
||||||
|
|
@ -247,24 +235,11 @@ class CustomerController extends Controller
|
||||||
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label()));
|
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label()));
|
||||||
} //end switch
|
} //end switch
|
||||||
|
|
||||||
/*
|
if ($request->action === "export") {
|
||||||
We create a callable to output the CSV in chunk
|
return $this->buildCsvDownloadResponse($response, $w);
|
||||||
with Symfony StreamResponse you can flush the body content if necessary
|
}
|
||||||
see Symfony documentation for more information
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * The flush value should depend on your CSV size.
|
return $this->importToMailjet($request, $w);
|
||||||
$flush_threshold = 1000;
|
|
||||||
|
|
||||||
$response->setCallback(function () use ($w, $flush_threshold) {
|
|
||||||
foreach ($w->chunk(1024) as $offset => $chunk) {
|
|
||||||
echo $chunk;
|
|
||||||
if ($offset % $flush_threshold === 0) {
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -313,6 +288,123 @@ class CustomerController extends Controller
|
||||||
->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id'));
|
->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the streamed HTTP response.
|
||||||
|
*
|
||||||
|
* @param CustomerExportType $exportType
|
||||||
|
* @return StreamedResponse
|
||||||
|
*/
|
||||||
|
private function buildStreamedResponse(CustomerExportType $exportType): StreamedResponse
|
||||||
|
{
|
||||||
|
// * We send the CSV using Symfony StreamedResponse.
|
||||||
|
$response = new StreamedResponse();
|
||||||
|
$response->headers->set('Content-Encoding', 'none');
|
||||||
|
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
|
||||||
|
$response->headers->set('Description', 'File Transfer');
|
||||||
|
$response->headers->set(
|
||||||
|
'Content-Disposition',
|
||||||
|
sprintf(
|
||||||
|
'attachment; filename="Export pour newsletter %s %s.csv"',
|
||||||
|
Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'),
|
||||||
|
Str::slug($exportType->label())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the CSV writer.
|
||||||
|
*
|
||||||
|
* @param string $action
|
||||||
|
* @return Writer
|
||||||
|
*/
|
||||||
|
private function buildCsvWriter(string $action): Writer
|
||||||
|
{
|
||||||
|
if ($action === 'export') {
|
||||||
|
$header = '"lastname";"firstname";"email";"hashcode"';
|
||||||
|
|
||||||
|
$w = Writer::createFromString($header . PHP_EOL);
|
||||||
|
$w->setDelimiter(';');
|
||||||
|
} else {
|
||||||
|
$header = 'lastname,firstname,email,hashcode';
|
||||||
|
|
||||||
|
$w = Writer::createFromString($header . PHP_EOL);
|
||||||
|
$w->setDelimiter(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
$w->setEnclosure('"');
|
||||||
|
$w->setOutputBOM(Writer::BOM_UTF8);
|
||||||
|
|
||||||
|
return $w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a download callback to the response.
|
||||||
|
*
|
||||||
|
* @param StreamedResponse $response
|
||||||
|
* @param Writer $writer
|
||||||
|
* @return StreamedResponse
|
||||||
|
*/
|
||||||
|
private function buildCsvDownloadResponse(StreamedResponse $response, Writer $writer): StreamedResponse
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
We create a callable to output the CSV in chunk
|
||||||
|
with Symfony StreamResponse you can flush the body content if necessary
|
||||||
|
see Symfony documentation for more information
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * The flush value should depend on your CSV size.
|
||||||
|
$flush_threshold = 1000;
|
||||||
|
|
||||||
|
$response->setCallback(function () use ($writer, $flush_threshold) {
|
||||||
|
foreach ($writer->chunk(1024) as $offset => $chunk) {
|
||||||
|
echo $chunk;
|
||||||
|
if ($offset % $flush_threshold === 0) {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import contacts to Mailjet
|
||||||
|
*
|
||||||
|
* @param CustomerExport $request
|
||||||
|
* @param Writer $writer
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
private function importToMailjet(CustomerExport $request, Writer $writer): \Illuminate\Http\RedirectResponse
|
||||||
|
{
|
||||||
|
$brandModel = Brand::find($request->brand);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$contactList = MailjetHelper::createContactList(
|
||||||
|
sprintf("%s - %s", now()->format('d/m/Y'), $brandModel->name)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mailjetListId = $contactList[0]['ID'];
|
||||||
|
|
||||||
|
$csvData = MailjetHelper::uploadCsvFile($mailjetListId, $writer->toString());
|
||||||
|
MailjetHelper::importContacts($mailjetListId, $csvData['ID']);
|
||||||
|
|
||||||
|
BrandContactList::createBrandContactList(
|
||||||
|
$brandModel,
|
||||||
|
$mailjetListId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return back()->with('success', 'Liste créée et contacts importés avec succès.');
|
||||||
|
} catch (MailjetException $e) {
|
||||||
|
report($e);
|
||||||
|
return back()->withErrors(sprintf(
|
||||||
|
'Impossible de créer la liste de contacts Mailjet : %s.',
|
||||||
|
$e->returnedMessage
|
||||||
|
));
|
||||||
|
}//end try
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all filters (brand, question, answer).
|
* Apply all filters (brand, question, answer).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,11 @@ class StoreBrandRequest extends FormRequest
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'name' => 'required|unique:brands,name|string|min:5|max:255',
|
'name' => 'required|unique:brands,name|string|min:5|max:255',
|
||||||
'url' => 'required|url|min:5|max:255',
|
'url' => 'required|url|min:5|max:255',
|
||||||
'url_claim' => 'required|url|min:5|max:255',
|
'url_claim' => 'required|url|min:5|max:255',
|
||||||
'domain' => 'required|unique:brands,domain|string|min:5|max:255',
|
'domain' => 'required|unique:brands,domain|string|min:5|max:255',
|
||||||
|
'newsletter_email' => 'nullable|email:rfc,strict,dns,spoof,filter',
|
||||||
];
|
];
|
||||||
return \array_merge(
|
return \array_merge(
|
||||||
$rules,
|
$rules,
|
||||||
|
|
@ -56,11 +57,12 @@ class StoreBrandRequest extends FormRequest
|
||||||
public function attributes(): array
|
public function attributes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => trans('validation.attributes.name'),
|
'name' => trans('validation.attributes.name'),
|
||||||
'logo' => trans('validation.custom.logo'),
|
'logo' => trans('validation.custom.logo'),
|
||||||
'url' => trans('validation.custom.url'),
|
'url' => trans('validation.custom.url'),
|
||||||
'url_claim' => trans('validation.custom.url_claim'),
|
'url_claim' => trans('validation.custom.url_claim'),
|
||||||
'domain' => trans('validation.custom.domain'),
|
'domain' => trans('validation.custom.domain'),
|
||||||
|
'newsletter_email' => trans('validation.custom.newsletter_email'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ class CustomerExport extends FormRequest
|
||||||
$fail(trans('Le type d\'export invalide'));
|
$fail(trans('Le type d\'export invalide'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
'action' => 'required|in:export,create_mailjet'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
app/Jobs/SyncEmailOptOuts.php
Normal file
53
app/Jobs/SyncEmailOptOuts.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Lib\Helpers\MailjetHelper;
|
||||||
|
use App\Models\BrandContactList;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
use App\Exceptions\MailjetException;
|
||||||
|
|
||||||
|
class SyncEmailOptOuts implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$contactLists = MailjetHelper::getContactLists();
|
||||||
|
|
||||||
|
if (empty($contactLists)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($contactLists as $contactList) {
|
||||||
|
$listId = $contactList['ID'];
|
||||||
|
$brandModel = BrandContactList::getBrandContactListByMailjetListId($listId);
|
||||||
|
|
||||||
|
if ($brandModel) {
|
||||||
|
$unsubscribedContacts = MailjetHelper::getUnsubscribedContacts($listId, Carbon::now()->subDay());
|
||||||
|
|
||||||
|
if (empty($unsubscribedContacts)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($unsubscribedContacts as $unsubscribedContact) {
|
||||||
|
$contact = MailjetHelper::getContact($unsubscribedContact['ContactID']);
|
||||||
|
|
||||||
|
MailjetHelper::unsubscribeContact($brandModel->brand, $contact[0]["Email"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (MailjetException $e) {
|
||||||
|
report($e);
|
||||||
|
return;
|
||||||
|
}//end try
|
||||||
|
}
|
||||||
|
}
|
||||||
286
app/Lib/Helpers/MailjetHelper.php
Normal file
286
app/Lib/Helpers/MailjetHelper.php
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Helpers;
|
||||||
|
|
||||||
|
use App\Models\Brand;
|
||||||
|
use App\Models\CustomerInfo;
|
||||||
|
use App\Models\SingleOptin;
|
||||||
|
use App\Exceptions\MailjetException;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Mailjet\Resources;
|
||||||
|
|
||||||
|
class MailjetHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get a campaign by ID
|
||||||
|
*
|
||||||
|
* @param integer $campaignId
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function getCampaign(int $campaignId): array
|
||||||
|
{
|
||||||
|
return Cache::remember("mailjet_campaign_{$campaignId}", 86400, function () use ($campaignId) {
|
||||||
|
$response = app('mailjetClient')->get(Resources::$Campaign, [
|
||||||
|
'id' => $campaignId
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to retrieve campaign
|
||||||
|
(Campaign ID: %d | Error Code: %d | Message: %s)',
|
||||||
|
$campaignId,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves contact lists
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function getContactLists(): array
|
||||||
|
{
|
||||||
|
$filters = [
|
||||||
|
'Limit' => 1000,
|
||||||
|
'Sort' => 'CreatedAt DESC',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = app('mailjetClient')->get(Resources::$Contactslist, [
|
||||||
|
'filters' => $filters
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to retrieve contact lists (Code: %d | Message: %s)',
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unsubscribed contacts for a given contact list
|
||||||
|
*
|
||||||
|
* @param integer $contactListId
|
||||||
|
* @param \Carbon\Carbon|null $lastActivity
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function getUnsubscribedContacts(int $contactListId, \Carbon\Carbon|null $lastActivity = null): array
|
||||||
|
{
|
||||||
|
$filters = [
|
||||||
|
'ContactsList' => $contactListId,
|
||||||
|
'Unsub' => true,
|
||||||
|
'Limit' => 1000,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!is_null($lastActivity)) {
|
||||||
|
$filters = array_merge(
|
||||||
|
$filters,
|
||||||
|
[
|
||||||
|
'LastActivityAt' => $lastActivity->toRfc3339String()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = app('mailjetClient')->get(Resources::$Listrecipient, [
|
||||||
|
'filters' => $filters
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to retrieve unsubscribed contacts for list %d (Code: %d | Message: %s)',
|
||||||
|
$contactListId,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contact for a given contact id
|
||||||
|
*
|
||||||
|
* @param integer $contactId
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function getContact(int $contactId): array
|
||||||
|
{
|
||||||
|
$response = app('mailjetClient')->get(Resources::$Contact, [
|
||||||
|
'id' => $contactId
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to retrieve contact (Contact ID: %d | Error Code: %d | Message: %s)',
|
||||||
|
$contactId,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Mailjet contact list
|
||||||
|
*
|
||||||
|
* @param string $listName
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function createContactList(string $listName): array
|
||||||
|
{
|
||||||
|
$response = app('mailjetClient')->post(Resources::$Contactslist, [
|
||||||
|
'body' => [
|
||||||
|
'Name' => $listName,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to create contact list (List Name: %s | Error Code: %d | Message: %s)',
|
||||||
|
$listName,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a CSV file
|
||||||
|
*
|
||||||
|
* @param integer $mailjetListId
|
||||||
|
* @param string $csvFile
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function uploadCsvFile(int $mailjetListId, string $csvFile): array
|
||||||
|
{
|
||||||
|
$response = app('mailjetClient')->post(Resources::$ContactslistCsvdata, [
|
||||||
|
'id' => $mailjetListId,
|
||||||
|
'body' => $csvFile,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to upload csv file (List ID: %d | Error Code: %d | Message: %s)',
|
||||||
|
$mailjetListId,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports contacts into a Mailjet contact list
|
||||||
|
*
|
||||||
|
* @param integer $listId
|
||||||
|
* @param integer $dataId
|
||||||
|
* @return array
|
||||||
|
* @throws \App\Exceptions\MailjetException If the Mailjet API call fails.
|
||||||
|
*/
|
||||||
|
public static function importContacts(int $listId, int $dataId): array
|
||||||
|
{
|
||||||
|
$response = app('mailjetClient')->post(Resources::$Csvimport, [
|
||||||
|
'body' => [
|
||||||
|
'ContactsListID' => $listId,
|
||||||
|
'DataID' => $dataId,
|
||||||
|
'Method' => "addnoforce"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->success()) {
|
||||||
|
return $response->getData();
|
||||||
|
} else {
|
||||||
|
throw new MailjetException(
|
||||||
|
$response,
|
||||||
|
sprintf(
|
||||||
|
'Mailjet API Error: Unable to import contacts
|
||||||
|
(List ID: %d | Data ID: %d | Error Code: %d | Message: %s)',
|
||||||
|
$listId,
|
||||||
|
$dataId,
|
||||||
|
$response->getStatus(),
|
||||||
|
$response->getBody()['ErrorMessage'] ?? $response->getReasonPhrase()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe a contact from a brand
|
||||||
|
*
|
||||||
|
* @param \App\Models\Brand $brandModel
|
||||||
|
* @param string $email
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception When optin status cannot be changed.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} //end if
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\Users\RoleEnum;
|
use App\Enums\Users\RoleEnum;
|
||||||
|
use App\Models\BrandContactList;
|
||||||
use App\Traits\Models\ActivityLog;
|
use App\Traits\Models\ActivityLog;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -10,14 +11,15 @@ use Illuminate\Support\Facades\Cache;
|
||||||
use Uwil\LaravelCommon\Helpers\FileHelper;
|
use Uwil\LaravelCommon\Helpers\FileHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property integer $id Brand id.
|
* @property integer $id Brand id.
|
||||||
* @property string $name Brand name.
|
* @property string $name Brand name.
|
||||||
* @property string $logo Brand logo.
|
* @property string $logo Brand logo.
|
||||||
* @property string $url Brand url link.
|
* @property string $url Brand url link.
|
||||||
* @property string $url_claim Brand associative url claim.
|
* @property string $url_claim Brand associative url claim.
|
||||||
* @property string $domain Brand domain.
|
* @property string $domain Brand domain.
|
||||||
* @property-read \Illuminate\Support\Carbon $created_at Brand created date.
|
* @property string $newsletter_email Brand associative newsletter email.
|
||||||
* @property-read \Illuminate\Support\Carbon $updated_at Brand updated date.
|
* @property-read \Illuminate\Support\Carbon $created_at Brand created date.
|
||||||
|
* @property-read \Illuminate\Support\Carbon $updated_at Brand updated date.
|
||||||
*
|
*
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Service>
|
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Service>
|
||||||
* $services Service HasMany relation.
|
* $services Service HasMany relation.
|
||||||
|
|
@ -46,6 +48,7 @@ class Brand extends Model
|
||||||
'url',
|
'url',
|
||||||
'url_claim',
|
'url_claim',
|
||||||
'domain',
|
'domain',
|
||||||
|
'newsletter_email',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -141,6 +144,19 @@ class Brand extends Model
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get brand by newsletter_email.
|
||||||
|
*
|
||||||
|
* @param string $newsletter_email
|
||||||
|
* @return Brand|null
|
||||||
|
*/
|
||||||
|
public static function getBrandByNewsletterEmail(string $newsletter_email): ?Brand
|
||||||
|
{
|
||||||
|
return Cache::remember("brand_{$newsletter_email}", 86400, function () use ($newsletter_email) {
|
||||||
|
return self::query()->where('newsletter_email', $newsletter_email)->first();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// * RELATIONS
|
// * RELATIONS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -232,4 +248,15 @@ class Brand extends Model
|
||||||
{
|
{
|
||||||
return $this->hasMany(ClaimMailTemplate::class);
|
return $this->hasMany(ClaimMailTemplate::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrandContactList has many relation.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function contactLists(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(BrandContactList::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
app/Models/BrandContactList.php
Normal file
65
app/Models/BrandContactList.php
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class BrandContactList extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'brand_id',
|
||||||
|
'mailjet_list_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
// * METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a brand contact list.
|
||||||
|
*
|
||||||
|
* @param Brand $brand
|
||||||
|
* @param integer $mailjetListId
|
||||||
|
* @return BrandContactList
|
||||||
|
*/
|
||||||
|
public static function createBrandContactList(Brand $brand, int $mailjetListId): BrandContactList
|
||||||
|
{
|
||||||
|
$brandContactList = self::create([
|
||||||
|
'brand_id' => $brand->getKey(),
|
||||||
|
'mailjet_list_id' => $mailjetListId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cache::put("brand_contact_list_{$mailjetListId}", $brandContactList, 86400);
|
||||||
|
|
||||||
|
return $brandContactList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get brand contact list by mailjet_list_id.
|
||||||
|
*
|
||||||
|
* @param integer $mailjetListId
|
||||||
|
* @return BrandContactList|null
|
||||||
|
*/
|
||||||
|
public static function getBrandContactListByMailjetListId(int $mailjetListId): ?BrandContactList
|
||||||
|
{
|
||||||
|
return Cache::remember("brand_contact_list_{$mailjetListId}", 86400, function () use ($mailjetListId) {
|
||||||
|
return self::query()->where('mailjet_list_id', $mailjetListId)->with('brand')->first();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// * RELATIONS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brand belongs to relation.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Brand::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Providers/MailjetServiceProvider.php
Normal file
34
app/Providers/MailjetServiceProvider.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class MailjetServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Mailjet client to call the API.
|
||||||
|
App::singleton('mailjetClient', function () {
|
||||||
|
$mailjetClient = new \Mailjet\Client(
|
||||||
|
config('services.mailjet.public'),
|
||||||
|
config('services.mailjet.secret'),
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
'version' => 'v3',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$mailjetClient->setConnectionTimeout(200);
|
||||||
|
$mailjetClient->setTimeout(200);
|
||||||
|
|
||||||
|
return $mailjetClient;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
"laravel/ui": "^4.2",
|
"laravel/ui": "^4.2",
|
||||||
"league/csv": "^9.7",
|
"league/csv": "^9.7",
|
||||||
|
"mailjet/mailjet-apiv3-php": "^1.6",
|
||||||
"mews/purifier": "^3.4",
|
"mews/purifier": "^3.4",
|
||||||
"propaganistas/laravel-phone": "^5.0",
|
"propaganistas/laravel-phone": "^5.0",
|
||||||
"sentry/sentry-laravel": "^4.10",
|
"sentry/sentry-laravel": "^4.10",
|
||||||
|
|
|
||||||
167
composer.lock
generated
167
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "1417f675b37867e72bdd34d5b0fcf629",
|
"content-hash": "70aad663cba0f4125a01bc8e53999abc",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "24slides/laravel-saml2",
|
"name": "24slides/laravel-saml2",
|
||||||
|
|
@ -3562,6 +3562,67 @@
|
||||||
],
|
],
|
||||||
"time": "2024-12-08T08:18:47+00:00"
|
"time": "2024-12-08T08:18:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mailjet/mailjet-apiv3-php",
|
||||||
|
"version": "v1.6.6",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mailjet/mailjet-apiv3-php.git",
|
||||||
|
"reference": "9d5cea25f347719d7df7909fb4a43a72bd4e5011"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mailjet/mailjet-apiv3-php/zipball/9d5cea25f347719d7df7909fb4a43a72bd4e5011",
|
||||||
|
"reference": "9d5cea25f347719d7df7909fb4a43a72bd4e5011",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"guzzlehttp/guzzle": "^7.4.4",
|
||||||
|
"php": "^8.1",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"symfony/validator": "^6.3|^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.4",
|
||||||
|
"php-coveralls/php-coveralls": "^2.0",
|
||||||
|
"phpcompatibility/php-compatibility": "*",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpunit/phpunit": "^11.4.0",
|
||||||
|
"squizlabs/php_codesniffer": "*"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Mailjet": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mailjet",
|
||||||
|
"email": "dev@mailjet.com",
|
||||||
|
"homepage": "https://dev.mailjet.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP wrapper for the Mailjet API",
|
||||||
|
"homepage": "https://github.com/mailjet/mailjet-apiv3-php/",
|
||||||
|
"keywords": [
|
||||||
|
"Mailjet",
|
||||||
|
"api",
|
||||||
|
"email",
|
||||||
|
"php",
|
||||||
|
"v3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mailjet/mailjet-apiv3-php/issues",
|
||||||
|
"source": "https://github.com/mailjet/mailjet-apiv3-php/tree/v1.6.6"
|
||||||
|
},
|
||||||
|
"time": "2025-12-24T15:55:48+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "masterminds/html5",
|
"name": "masterminds/html5",
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
|
|
@ -7953,6 +8014,110 @@
|
||||||
],
|
],
|
||||||
"time": "2024-09-25T14:21:43+00:00"
|
"time": "2024-09-25T14:21:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/validator",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/validator.git",
|
||||||
|
"reference": "8f73cbddae916756f319b3e195088da216f0f12f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/validator/zipball/8f73cbddae916756f319b3e195088da216f0f12f",
|
||||||
|
"reference": "8f73cbddae916756f319b3e195088da216f0f12f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/polyfill-ctype": "~1.8",
|
||||||
|
"symfony/polyfill-mbstring": "~1.0",
|
||||||
|
"symfony/polyfill-php83": "^1.27",
|
||||||
|
"symfony/translation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/lexer": "<1.1",
|
||||||
|
"symfony/dependency-injection": "<6.4",
|
||||||
|
"symfony/doctrine-bridge": "<7.0",
|
||||||
|
"symfony/expression-language": "<6.4",
|
||||||
|
"symfony/http-kernel": "<6.4",
|
||||||
|
"symfony/intl": "<6.4",
|
||||||
|
"symfony/property-info": "<6.4",
|
||||||
|
"symfony/translation": "<6.4.3|>=7.0,<7.0.3",
|
||||||
|
"symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3",
|
||||||
|
"symfony/yaml": "<6.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||||
|
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/config": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/mime": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/property-info": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/string": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/translation": "^6.4.3|^7.0.3|^8.0",
|
||||||
|
"symfony/type-info": "^7.1.8",
|
||||||
|
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Validator\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/",
|
||||||
|
"/Resources/bin/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides tools to validate values",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/validator/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-30T12:55:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/var-dumper",
|
"name": "symfony/var-dumper",
|
||||||
"version": "v7.2.0",
|
"version": "v7.2.0",
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ return [
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
App\Providers\TranslationServiceProvider::class,
|
App\Providers\TranslationServiceProvider::class,
|
||||||
App\Providers\ViewServiceProvider::class,
|
App\Providers\ViewServiceProvider::class,
|
||||||
|
App\Providers\MailjetServiceProvider::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,8 @@ return [
|
||||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'mailjet' => [
|
||||||
|
'public' => env('MJ_APIKEY_PUBLIC'),
|
||||||
|
'secret' => env('MJ_APIKEY_PRIVATE'),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ class BrandFactory extends Factory
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => $this->faker->unique()->company,
|
'name' => $this->faker->unique()->company,
|
||||||
'url' => $this->faker->url,
|
'url' => $this->faker->url,
|
||||||
'url_claim' => $this->faker->url,
|
'url_claim' => $this->faker->url,
|
||||||
'domain' => $this->faker->unique()->domainName,
|
'domain' => $this->faker->unique()->domainName,
|
||||||
|
'newsletter_email' => $this->faker->email
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
database/migrations/2026_04_03_152802_alter_brands_table.php
Normal file
28
database/migrations/2026_04_03_152802_alter_brands_table.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('brands', function (Blueprint $table) {
|
||||||
|
$table->string('newsletter_email')->nullable()->comment('Brand associative newsletter email.')->after('domain');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('brands', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('newsletter_email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('brand_contact_lists', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('brand_id')->comment('Brand associated.')->constrained('brands')
|
||||||
|
->onUpdate('cascade')->onDelete('cascade');
|
||||||
|
$table->unsignedBigInteger('mailjet_list_id')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('brand_contact_lists');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -32,6 +32,8 @@ data:
|
||||||
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"
|
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"
|
||||||
SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}"
|
SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}"
|
||||||
UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}"
|
UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}"
|
||||||
|
MJ_APIKEY_PUBLIC: "${APP_MJ_APIKEY_PUBLIC}"
|
||||||
|
MJ_APIKEY_PRIVATE: "${APP_MJ_APIKEY_PRIVATE}"
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|
@ -82,7 +84,12 @@ spec:
|
||||||
lifecycle:
|
lifecycle:
|
||||||
preStop:
|
preStop:
|
||||||
exec:
|
exec:
|
||||||
command: ["/bin/sh","-c","kill -s TERM 1; while test $(ps ax | grep -v grep | grep bash | wc -l) -gt 0; do sleep 2s; done"]
|
command:
|
||||||
|
[
|
||||||
|
"/bin/sh",
|
||||||
|
"-c",
|
||||||
|
"kill -s TERM 1; while test $(ps ax | grep -v grep | grep bash | wc -l) -gt 0; do sleep 2s; done",
|
||||||
|
]
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: "${APP_CI_CPU_LIMIT}"
|
cpu: "${APP_CI_CPU_LIMIT}"
|
||||||
|
|
|
||||||
|
|
@ -222,20 +222,21 @@ return [
|
||||||
'year' => 'year',
|
'year' => 'year',
|
||||||
],
|
],
|
||||||
'custom' => [
|
'custom' => [
|
||||||
'event' => 'event',
|
'event' => 'event',
|
||||||
'model' => 'target element',
|
'model' => 'target element',
|
||||||
'order' => 'order',
|
'order' => 'order',
|
||||||
'publication' => 'publication',
|
'publication' => 'publication',
|
||||||
'activated' => 'activated',
|
'activated' => 'activated',
|
||||||
'target' => 'target',
|
'target' => 'target',
|
||||||
'response_type' => 'response type',
|
'response_type' => 'response type',
|
||||||
'reference' => 'reference',
|
'reference' => 'reference',
|
||||||
'type' => 'type',
|
'type' => 'type',
|
||||||
'status' => 'status',
|
'status' => 'status',
|
||||||
'label' => 'label',
|
'label' => 'label',
|
||||||
'domain' => 'domain',
|
'domain' => 'domain',
|
||||||
'logo' => 'logo',
|
'logo' => 'logo',
|
||||||
'url' => 'url',
|
'url' => 'url',
|
||||||
'url_claim' => 'claim url',
|
'url_claim' => 'claim url',
|
||||||
|
'newsletter_email' => 'email address for the newsletter',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -222,20 +222,21 @@ return [
|
||||||
'year' => 'année',
|
'year' => 'année',
|
||||||
],
|
],
|
||||||
'custom' => [
|
'custom' => [
|
||||||
'event' => 'évènement',
|
'event' => 'évènement',
|
||||||
'model' => 'élément ciblé',
|
'model' => 'élément ciblé',
|
||||||
'order' => 'order',
|
'order' => 'order',
|
||||||
'publication' => 'publication',
|
'publication' => 'publication',
|
||||||
'activated' => 'activé',
|
'activated' => 'activé',
|
||||||
'target' => 'cible',
|
'target' => 'cible',
|
||||||
'response_type' => 'type de réponse',
|
'response_type' => 'type de réponse',
|
||||||
'reference' => 'référence',
|
'reference' => 'référence',
|
||||||
'type' => 'type',
|
'type' => 'type',
|
||||||
'status' => 'status',
|
'status' => 'status',
|
||||||
'label' => 'libellé',
|
'label' => 'libellé',
|
||||||
'domain' => 'domaine',
|
'domain' => 'domaine',
|
||||||
'logo' => 'logo',
|
'logo' => 'logo',
|
||||||
'url' => 'lien',
|
'url' => 'lien',
|
||||||
'url_claim' => 'lien réclamation',
|
'url_claim' => 'lien réclamation',
|
||||||
|
'newsletter_email' => 'adresse email pour la newsletter',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,31 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
{{-- NEWSLETTER_EMAIL --}}
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<fieldset class="h-100 border p-3">
|
||||||
|
<legend>{{ str(__('validation.custom.newsletter_email'))->ucfirst() }}</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
@include('bo.partials.inputs.varchar', [
|
||||||
|
'id' => 'brandNewsletterEmail',
|
||||||
|
'label' => __(':field de la :model', [
|
||||||
|
'field' => str(__('validation.custom.newsletter_email'))->ucfirst(),
|
||||||
|
'model' => str(__('models.classes.brands'))->singular(),
|
||||||
|
]),
|
||||||
|
'hasWordCounter' => true,
|
||||||
|
'name' => 'newsletter_email',
|
||||||
|
'required' => false,
|
||||||
|
'model' => $brandModel ?? null,
|
||||||
|
'placeholder' => 'no-reply@example.com',
|
||||||
|
'helper' => __('Adresse email utilisée pour l\'envoi de la newsletter.'),
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
{{-- IMAGE --}}
|
{{-- IMAGE --}}
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
<fieldset class="h-100 border p-3">
|
<fieldset class="h-100 border p-3">
|
||||||
<legend>{{ str(__('validation.attributes.image'))->ucfirst() }}</legend>
|
<legend>{{ str(__('validation.attributes.image'))->ucfirst() }}</legend>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center mt-4">
|
<div class="row justify-content-center mt-4">
|
||||||
<div class="col-lg-6 col-md-10 col-12 text-center">
|
<div class="col-lg-6 col-md-10 col-12 text-center text-lg-end">
|
||||||
<button class="btn btn-primary" data-bs-tooltip="tooltip" type="submit"
|
<button class="btn btn-primary" data-bs-tooltip="tooltip" type="submit" name="action" value="export"
|
||||||
title="{{ __('Cliquer pour exporter la liste de client pour une newsletter') }}">
|
title="{{ __('Cliquer pour exporter la liste de client pour une newsletter') }}">
|
||||||
{{ __('Exporter') }}
|
{{ __('Exporter au format CSV') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 col-md-10 col-12 text-center text-lg-start">
|
||||||
|
<button class="btn btn-success" data-bs-tooltip="tooltip" type="submit" name="action" value="create_mailjet"
|
||||||
|
title="{{ __('Cliquer pour importer la liste de client sur mailjet') }}">
|
||||||
|
{{ __('Importer sur Mailjet') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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\OptinController as V4OptinController;
|
||||||
use App\Http\Controllers\Api\V4\QuestionController as V4QuestionController;
|
use App\Http\Controllers\Api\V4\QuestionController as V4QuestionController;
|
||||||
use App\Http\Controllers\Api\V4\StatsController as V4StatsController;
|
use App\Http\Controllers\Api\V4\StatsController as V4StatsController;
|
||||||
|
use App\Http\Controllers\Api\WebhookController;
|
||||||
use Illuminate\Support\Facades\Route;
|
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('/claim', [V4ClaimController::class, 'store'])->name('claim.store');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::post('/webhook/mailjet', [WebhookController::class, 'handleMailjetWebhook'])->name('webhook.mailjet');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue