feat: ✨ add crm + newsletter subscription + unsubscribe page

This commit is contained in:
Karl Muller 2025-04-29 13:10:15 +00:00 • committed by Noah Lancien
parent a7a227a669
commit c6a9e2135f
20 changed files with 589 additions and 20 deletions

View file

@ -52,3 +52,8 @@ SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=
# CRM
CRM_ENDPOINT=
CRM_PASSWORD=
CRM_TOKEN=

View file

@ -9,6 +9,10 @@ use App\Mail\ContactFormMail;
use App\Models\StaticPage;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use LDC\LDCCRM\Enums\ApiError;
use LDC\LDCCRM\Exceptions\ApiErrorException;
use LDC\LDCCRM\Exceptions\CrmException;
use LDC\LDCCRM\Models\SingleOptin;
class ContactController extends Controller
{
@ -34,6 +38,28 @@ class ContactController extends Controller
{
Mail::to(config('mail.contactsList.contactform'))->send(new ContactFormMail($request));
if ($request->opt_in) {
try {
SingleOptin::setStatus($request->email, true);
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::singleOptinCustomerExists)) {
return back()
->withErrors(['opt_in' => trans('Vous êtes déjà inscrit avec cette adresse email')])
->withInput();
}
\report($e);
return back()
->withErrors(['opt_in' => trans('Une erreur est survenue, merci de réessayer ultérieurement')])
->withInput();
} catch (CrmException $e) {
\report($e);
return back()
->withErrors(['opt_in' => trans('Une erreur est survenue, merci de réessayer ultérieurement')])
->withInput();
}
} //end if
Session::forget('saveProductInBasket');
Session::flash('matomo_generate_lead');

View file

@ -3,10 +3,19 @@
namespace App\Http\Controllers\Fo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Fo\Newsletter\SingleOptinRequest;
use App\Http\Requests\Fo\Newsletter\UnsubscribeNewsletterRequest;
use App\Models\News;
use App\Models\Product;
use App\Models\Range;
use App\Models\Slide;
use Illuminate\Support\Facades\Log;
use LDC\LDCCRM\Enums\ApiError;
use LDC\LDCCRM\Exceptions\ApiErrorException;
use LDC\LDCCRM\Exceptions\CrmException;
use LDC\LDCCRM\Exceptions\ValidationException;
use LDC\LDCCRM\Models\Customer;
use LDC\LDCCRM\Models\SingleOptin;
class HomeController extends Controller
{
@ -55,4 +64,79 @@ class HomeController extends Controller
'news'
));
}
/**
* Call the CRM to add a single optin
*
* @param \App\Http\Requests\Fo\Newsletter\SingleOptinRequest $request
* @return string|array|null|\Illuminate\Contracts\Translation\Translator
* @throws \LDC\LDCCRM\Exceptions\ValidationException ValidationException.
* @throws \Illuminate\Contracts\Container\BindingResolutionException BindingResolutionException.
*/
public function addOptIn(SingleOptinRequest $request)
{
try {
SingleOptin::setStatus($request->email, true);
return \response(trans("Votre inscription à la newsletter a été prise en compte"), 200);
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::singleOptinCustomerExists)) {
return trans('Vous êtes déjà inscrit avec cette adresse email');
}
\report($e);
return \response(trans('Une erreur est survenue, merci de réessayer ultérieurement'), 404);
} catch (CrmException $e) {
\report($e);
return \response(trans('Une erreur est survenue, merci de réessayer ultérieurement'), 404);
}
}
/**
* unsubscribeNewsletter method
*
* @param \App\Http\Requests\Fo\Newsletter\UnsubscribeNewsletterRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function unsubscribeNewsletter(UnsubscribeNewsletterRequest $request): \Illuminate\Contracts\View\View
{
$error = null;
$hashcode = $request->hashcode;
try {
SingleOptin::unsetStatusWithHashcode($hashcode);
} catch (ApiErrorException $e) {
Log::debug($e->getMessage());
if ($e->getApiError()->equals(ApiError::modelNotFound)) {
if ($customer = Customer::find($hashcode)) {
$customer->opt_in = false;
// ! Les données peuvent être parsés en retour de la crm et inconsitantes (Héritage vielle crm).
try {
$customer->save();
} catch (ValidationException $e) {
$error = null;
}
} else {
abort(404);
}
} else {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
$error = true;
}
} catch (CrmException $e) {
Log::debug($e->getMessage());
$error = true;
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
} //end try
if (!is_null($error)) {
return view('fo.pages.newsletter-unsubscribe')->with(
'error',
trans('Une erreur est survenue, merci de réessayer ultérieurement')
);
}
return view('fo.pages.newsletter-unsubscribe');
}
}

View file

@ -20,6 +20,18 @@ class ContactRequest extends FormRequest
return true;
}
/**
* Prepare the data for validation.
*
* @return void
*/
public function prepareForValidation(): void
{
$this->merge([
'opt_in' => $this->boolean('opt_in'),
]);
}
/**
* Get the validation rules that apply to the request.
*
@ -40,7 +52,8 @@ class ContactRequest extends FormRequest
'city' => 'nullable|string|max:255',
'message' => 'required|string|min:20|max:255',
'callTime' => ['required', new Enum(ContactTimeEnum::class)],
'brigade' => 'nullable|boolean',
'brigade' => 'nullable|boolean',
'opt_in' => 'required|boolean',
];
}
@ -81,7 +94,8 @@ class ContactRequest extends FormRequest
'city' => trans('ville'),
'message' => trans('message'),
'callTime' => trans('préférence de rappel'),
'brigade' => trans('La Brigade')
'brigade' => trans('La Brigade'),
'opt_in' => trans('newsletter'),
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Fo\Newsletter;
use Illuminate\Foundation\Http\FormRequest;
class SingleOptinRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email:rfc,dns,spoof',
];
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Fo\Newsletter;
use Illuminate\Foundation\Http\FormRequest;
class UnsubscribeNewsletterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'hashcode' => 'required|uuid'
];
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use LDC\LDCCRM\CrmApiV4;
class CrmApiServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot(): void
{
App::singleton('crmAPI', function () {
return new CrmApiV4(
config('crm.endpoint'),
config('crm.token'),
config('crm.password')
);
});
}
}

View file

@ -22,6 +22,7 @@
"laravel/framework": "^11.0",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"ldc/ldccrmv4client": "^4.0",
"mews/purifier": "^3.4",
"propaganistas/laravel-phone": "^5.0.3",
"sentry/sentry-laravel": "^4.9",

187
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2877466a2db551f8fec6c811bd7d59c4",
"content-hash": "82e60c1c2f09c728135cd60aa5bc9ab5",
"packages": [
{
"name": "24slides/laravel-saml2",
@ -2405,6 +2405,63 @@
},
"time": "2025-01-28T15:15:29+00:00"
},
{
"name": "ldc/ldccrmv4client",
"version": "4.0.9",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/crm-client.git",
"reference": "b54f61add50e93c68d26cadbe55d29216fcfc666"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/26/packages/composer/archives/ldc/ldccrmv4client.zip?sha=b54f61add50e93c68d26cadbe55d29216fcfc666",
"reference": "b54f61add50e93c68d26cadbe55d29216fcfc666",
"shasum": ""
},
"require": {
"doctrine/inflector": "^2.0",
"ext-curl": "*",
"guzzlehttp/guzzle": "^7.4",
"php": "^8.2",
"respect/validation": "^2.2"
},
"require-dev": {
"orchestra/testbench": "^8.10",
"phpunit/phpunit": "9.6",
"slevomat/coding-standard": "^8.10",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
"psr-4": {
"LDC\\LDCCRM\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LDC\\LDCCRM\\Tests\\": "tests"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Jérémy Munsch",
"email": "jeremy.munsch@ldc.fr",
"homepage": "https://jeremydev.ovh"
}
],
"description": "LDC CRM Client V4",
"homepage": "https://github.com/kwaadpepper",
"keywords": [
"CRM",
"Client",
"LDC",
"Laravel"
]
},
{
"name": "league/commonmark",
"version": "2.6.1",
@ -5041,6 +5098,128 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "respect/stringifier",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/Respect/Stringifier.git",
"reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
"reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.8",
"malukenho/docheader": "^0.1.7",
"phpunit/phpunit": "^6.4"
},
"type": "library",
"autoload": {
"files": [
"src/stringify.php"
],
"psr-4": {
"Respect\\Stringifier\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Respect/Stringifier Contributors",
"homepage": "https://github.com/Respect/Stringifier/graphs/contributors"
}
],
"description": "Converts any value to a string",
"homepage": "http://respect.github.io/Stringifier/",
"keywords": [
"respect",
"stringifier",
"stringify"
],
"support": {
"issues": "https://github.com/Respect/Stringifier/issues",
"source": "https://github.com/Respect/Stringifier/tree/0.2.0"
},
"time": "2017-12-29T19:39:25+00:00"
},
{
"name": "respect/validation",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/Respect/Validation.git",
"reference": "d1575eb4dde7b06f8dbc5e8d84dffb87931a40bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Respect/Validation/zipball/d1575eb4dde7b06f8dbc5e8d84dffb87931a40bd",
"reference": "d1575eb4dde7b06f8dbc5e8d84dffb87931a40bd",
"shasum": ""
},
"require": {
"php": ">=8.1",
"respect/stringifier": "^0.2.0",
"symfony/polyfill-mbstring": "^1.2"
},
"require-dev": {
"egulias/email-validator": "^3.0",
"giggsey/libphonenumber-for-php-lite": "^8.13",
"malukenho/docheader": "^1.0",
"mikey179/vfsstream": "^1.6",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-phpunit": "^1.3",
"phpunit/phpunit": "^9.6",
"psr/http-message": "^1.0",
"respect/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"suggest": {
"egulias/email-validator": "Improves the Email rule if available",
"ext-bcmath": "Arbitrary Precision Mathematics",
"ext-fileinfo": "File Information",
"ext-mbstring": "Multibyte String Functions",
"giggsey/libphonenumber-for-php-lite": "Enables the phone rule if available"
},
"type": "library",
"autoload": {
"psr-4": {
"Respect\\Validation\\": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Respect/Validation Contributors",
"homepage": "https://github.com/Respect/Validation/graphs/contributors"
}
],
"description": "The most awesome validation engine ever created for PHP",
"homepage": "http://respect.github.io/Validation/",
"keywords": [
"respect",
"validation",
"validator"
],
"support": {
"issues": "https://github.com/Respect/Validation/issues",
"source": "https://github.com/Respect/Validation/tree/2.4.1"
},
"time": "2025-01-11T00:06:34+00:00"
},
{
"name": "robrichards/xmlseclibs",
"version": "3.1.3",
@ -12275,12 +12454,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.3"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View file

@ -179,6 +179,7 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ViewServiceProvider::class,
App\Providers\CrmApiServiceProvider::class,
],
/*

7
config/crm.php Normal file
View file

@ -0,0 +1,7 @@
<?php
return [
'endpoint' => env('CRM_ENDPOINT'),
'token' => env('CRM_TOKEN'),
'password' => env('CRM_PASSWORD')
];

View file

@ -33,7 +33,9 @@ data:
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"
SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}"
SAML2_DEFAULT_TENANT_UUID: "${APP_SAML2_DEFAULT_TENANT_UUID}"
CRM_ENDPOINT: "${APP_CRM_ENDPOINT}"
CRM_PASSWORD: "${APP_CRM_PASSWORD}"
CRM_TOKEN: "${APP_CRM_TOKEN}"
---
apiVersion: v1
kind: PersistentVolumeClaim

View file

@ -3,11 +3,21 @@ footer {
background-color: $color-bg-footer;
a { outline: none; }
a:hover { color: $secondary; }
a {
outline: none;
}
ul { list-style-type: none; }
.img-slogan-wrapper { max-width: 250px; }
a:hover {
color: $secondary;
}
ul {
list-style-type: none;
}
.img-slogan-wrapper {
max-width: 250px;
}
.deactivatable {
position: relative;
@ -25,8 +35,14 @@ footer {
rotate: 0deg;
transition: rotate 0.3s ease-in-out;
}
.deactivatable:hover::after { background: no-repeat center/cover url("@/images/fleche-hover.png"); }
.deactivatable.active::after { rotate: 180deg; }
.deactivatable:hover::after {
background: no-repeat center/cover url("@/images/fleche-hover.png");
}
.deactivatable.active::after {
rotate: 180deg;
}
}
// LLLLLLLLLLL GGGGGGGGGGGGG
@ -45,26 +61,36 @@ footer {
// L::::::::::::::::::::::L GG:::::::::::::::G
// L::::::::::::::::::::::L GGG::::::GGG:::G
// LLLLLLLLLLLLLLLLLLLLLLLL GGGGGG GGGG
@include media-breakpoint-up(lg) { // >= 992px
@include media-breakpoint-up(lg) {
// >= 992px
footer {
.banner-wrapper {
> .row {
>.row {
width: 100%;
flex-direction: row-reverse !important;
}
.team-wrapper { flex: 3; }
.team-wrapper {
flex: 3;
}
.social-wrapper {
flex: 2;
text-align: left !important;
}
.newsletter-wrapper {
flex: 2;
}
}
.deactivatable {
position: static;
&::after { display: none; }
&::after {
display: none;
}
}
}
}

View file

@ -1,3 +1,6 @@
import route from "../../modules/route";
const laravelRoute = route.methods.route;
window.addEventListener("DOMContentLoaded", () => {
// BOUTON HAMBURGER
@ -102,6 +105,51 @@ window.addEventListener("DOMContentLoaded", () => {
}));
}
// Subscribe to newsletter
const newsletterForms = document.querySelectorAll(".newsletter-wrapper");
newsletterForms.forEach(form => {
const emailInput = form.querySelector(".newsletter-email") as HTMLInputElement;
const submitBtn = form.querySelector(".newsletter-submit");
const messageDiv = form.querySelector(".newsletter-message");
if (emailInput && submitBtn && messageDiv) {
submitBtn.addEventListener("click", function (e) {
e.preventDefault();
messageDiv.classList.remove("alert", "alert-success", "alert-danger");
messageDiv.textContent = "";
const addSingleOptinRoute = laravelRoute("fo.add-single-optin");
if (!addSingleOptinRoute) return;
window.axios.post(addSingleOptinRoute, {
email: emailInput.value,
})
.then(response => {
messageDiv.classList.remove("d-none");
messageDiv.classList.add("alert-success", "alert");
messageDiv.textContent = String(response.data);
emailInput.value = "";
})
.catch(error => {
messageDiv.classList.remove("d-none");
messageDiv.classList.add("alert-danger", "alert");
if (error.response.status == 422) {
const errors = error.response.data.errors;
for (const errorKey in errors) {
const errorP = document.createElement("p");
errorP.classList.add("m-0");
errorP.textContent = errors[errorKey];
messageDiv.appendChild(errorP);
}
} else {
messageDiv.textContent = error.response.data;
}
});
});
}
});
// PAGE NOS ACTUALITES
// ON AFFICHE OU ON CACHE LES SOUS-MENUS DU HEADER

View file

@ -0,0 +1,23 @@
<div class="col newsletter-wrapper my-lg-5 mt-5">
<div class="d-block d-md-flex align-items-center justify-content-center gap-2">
<div>
<p class="text-start">
{{ __('Abonnez-vous à notre') }} <br />
<span class="fw-bold fs-2 text-uppercase">{{ __('Newsletter') }}</span>
</p>
</div>
<div class="newsletter-form">
<div class="input-group rounded-4 mb-3 overflow-hidden">
<input class="newsletter-email form-control rounded-start text-primary border-0 bg-white py-3" type="text"
aria-label="Votre email" aria-describedby="basic-addon2" placeholder="{{ __('Votre email') }}">
<button
class="newsletter-submit btn bg-noir input-group-text text-uppercase fs-4 fw-bold rounded-0 px-3">{{ __('OK') }}</button>
</div>
</div>
</div>
<div class="d-none newsletter-message my-3"></div>
<p class="text-center">Pour toute information complémentaire concernant vos données personnelles et vos droits, merci de consulter
notre <a>Charte de
confidentialité.</a></p>
</div>

View file

@ -17,4 +17,5 @@
'{{ route('fo.product.add-product-to-basket', ['publishedProduct' => 'PRODUCT']) }}';
window.__SYSTEM._routes['product.remove-product-from-basket'] =
'{{ route('fo.product.remove-product-from-basket', ['slug' => 'SLUG']) }}';
window.__SYSTEM._routes['fo.add-single-optin'] = '{{ route('fo.add-single-optin') }}';
</script>

View file

@ -133,8 +133,23 @@
</div>
@endforeach
</div>
<div class="row wrapper-sm p-3">
<div class="col col-md-8 lh-sm px-0 py-2">
<div class="form-check text-start">
<input class="form-check-input @if ($errors->has('opt_in')) is-invalid @endif me-1" id="check-optin"
name="opt_in" type="checkbox" @checked(old('opt_in'))>
<label class="form-check-label w-fit" for="check-optin" role="button">
{{ __('Je souhaite m’inscrire à la newsletter') }} {{ Config::get('theme.site_name') }}
</label>
</div>
@foreach ($errors->get('opt_in') as $message)
<div class="invalid-feedback d-block">{{ $message }}</div>
@endforeach
</div>
</div>
<div class="row wrapper-sm mb-3">
<div class="rgpg-wrapper d-flex align-items-center ff-poppins-regular fs-xxsm mt-1 p-3">
<div class="rgpg-wrapper d-flex align-items-center ff-poppins-regular fs-xxsm px-3 py-1">
<p>
{{ __('Les informations recueillies à partir de ce formulaire sont traitées par le Responsable de Traitement pour donner suite à votre demande de contact. Pour connaître vos droits, vous pouvez consulter la') }}
<a class="fw-bold text-decoration-underline" href="{{ route('fo.policies') }}"

View file

@ -0,0 +1,42 @@
@php
$staticPageModel = new stdClass();
$staticPageModel->title = __('Désincription à la newsletter');
$staticPageModel->subtitle = __('Vous partez déjà ?');
$staticPageModel->picture = null;
$staticPageModel->picture_title = '';
$staticPageModel->picture_alt = '';
@endphp
@extends('fo.layout', ['page' => 'unsubscribe'])
@section('title', __('Désincription à la newsletter'))
@section('description', __('Page de désincription à la newsletter du site le Gaulois'))
@section('bannerHeader')
@include('fo.partials.banner-header', [
'title' => $staticPageModel->title,
'description' => '',
])
@endsection
@section('content')
{{-- HEADER --}}
<div class="container mb-3 mt-5 pt-5">
<div class="row mb-5">
<div @class([
'col-6 mx-auto alert fw-medium',
'alert-danger' => isset($error),
'alert-success' => !isset($error),
])>
<p>
@isset($error)
{{ $error }}
@else
{{ __("Votre désinscription a été effectuée avec succès. Si vous changez d'avis, n'hésitez pas à vous réinscrire !") }}
@endisset
</p>
</div>
</div>
</div>
@endsection

View file

@ -21,6 +21,7 @@
</div>
</div>
</div>
<x-fo.newsletter.newsletter-subscribe />
<div class="col social-wrapper my-lg-5 mt-5 text-center">
<p class="ff-poppins-bold fs-banner-medium">
{{ __('Restez connecté') }}<br>

View file

@ -19,6 +19,12 @@ Route::name('fo.')
->group(function () {
Route::get('/', [HomeController::class, 'index'])->name('homepage');
// * NEWSLETTER
Route::post('/optin', [HomeController::class, 'addOptIn'])->name('add-single-optin');
Route::get('/{unsubscribe}', [HomeController::class, 'unsubscribeNewsletter'])
->where('unsubscribe', 'unsubscribe|unsubscribe.php|unsubscribe.html|newsletter')
->name('unsubscribeNewsletter');
Route::prefix('/notre-adn')
->name('adn.')
->group(function () {
@ -112,10 +118,12 @@ Route::name('fo.')
Route::get('/{publishedProduct:slug}', [ProductsController::class, 'show'])
->name('show');
Route::post('/{publishedProduct:slug}/ajout-au-panier', [
ProductsController::class, 'addProductToBasket'
ProductsController::class,
'addProductToBasket'
])->name('add-product-to-basket');
Route::get('/{publishedProduct:slug}/print-commercial-sheet', [
ProductsController::class, 'printCommericalSheet'
ProductsController::class,
'printCommericalSheet'
])->name('print-commercial-sheet');
Route::post('/{slug}/retrait-du-panier', [ProductsController::class, 'removeProductFromBasket'])
->name('remove-product-from-basket');