diff --git a/.env.example b/.env.example index 809a585c..4658b0bc 100644 --- a/.env.example +++ b/.env.example @@ -75,11 +75,6 @@ GOOGLE_RECAPTCHA_PUBLIC= GOOGLE_RECAPTCHA_PRIVATE= GOOGLE_RECAPTCHA_VERSION= -## MERCANET -MERCANET_SECRET= -MERCANET_MERCHAND_ID= -MERCANET_ENV= - ## TELESCOPE TELESCOPE_ENABLED= @@ -101,3 +96,9 @@ REDIS_USERNAME= REDIS_PASSWORD= REDIS_PREFIX= REDIS_SCHEME= + +## AXEPTA +AXEPTA_ENDPOINT= +AXEPTA_MERCHANT_ID= +AXEPTA_APIKEY= +AXEPTA_HMAC= \ No newline at end of file diff --git a/app/Enums/Mercanet/PaymentResponse.php b/app/Enums/Mercanet/PaymentResponse.php deleted file mode 100644 index 35ebcfe5..00000000 --- a/app/Enums/Mercanet/PaymentResponse.php +++ /dev/null @@ -1,44 +0,0 @@ -name => 'Situation normale suivie du processus normal - d\'affichage des pages de paiement.', - self::invalidIds->name => 'L’identifiant commerçant ou le contrat acquéreur sont invalides.', - self::invalidParameters->name => 'Les paramètres de la transaction sont invalides. Vérifiez les - paramètres de la requête.', - self::canceled->name => 'Transaction annulée', - self::invalidRequestFormat->name => 'Le format de la requête est invalide.', - self::securityIssue->name => 'Problème de sécurité, par ex. le sceau calculé est incorrect.', - self::transactionAlreadyExists->name => 'La transaction existe déjà.', - self::expired->name => 'Délais expiré', - self::unreachableService->name => 'Service temporairement indisponible', - self::internalIssue->name => 'Erreur interne, eg: probleme appel CURL' - ]; - //phpcs:enable -} diff --git a/app/Enums/Mercanet/RedirectionStatusCode.php b/app/Enums/Mercanet/RedirectionStatusCode.php deleted file mode 100644 index d50358da..00000000 --- a/app/Enums/Mercanet/RedirectionStatusCode.php +++ /dev/null @@ -1,35 +0,0 @@ -name => 'Redirection Acceptée', - self::requestError03->name => 'Problème de request code 03', - self::requestError12->name => 'Problème de request code 12', - self::requestError30->name => 'Problème de request code 30', - self::requestError34->name => 'Problème de request code 34', - self::referenceAlreadyUsed->name => 'La référence de transaction à déja été utilisée', - self::serviceDisponibiltyIssue->name => 'Problème de disponibilité du service de paiement', - self::internalIssue->name => 'Erreur interne, eg: probleme appel CURL' - ]; -} diff --git a/app/Enums/Mercanet/TransactionResponse.php b/app/Enums/Mercanet/TransactionResponse.php deleted file mode 100644 index a60429d8..00000000 --- a/app/Enums/Mercanet/TransactionResponse.php +++ /dev/null @@ -1,37 +0,0 @@ -name => 'Paiement accepté', - self::refused->name => 'Paiement refusé', - self::canceled->name => 'Paiement annulé', - self::fraud->name => 'Paiement refusé pour fraude', - self::maxReached->name => 'Paiement refusé pour nombre max essais atteint', - self::technicalIssue->name => 'Paiement refusé pour problème technique', - self::duplicate->name => 'Transaction dupliquée', - self::expired->name => 'Paiement expiré', - self::internalIssue->name => 'Erreur interne, eg: probleme appel CURL' - ]; -} diff --git a/app/Http/Controllers/Club/ShopController.php b/app/Http/Controllers/Club/ShopController.php index 8fdc9040..b7f392ab 100644 --- a/app/Http/Controllers/Club/ShopController.php +++ b/app/Http/Controllers/Club/ShopController.php @@ -2,9 +2,6 @@ namespace App\Http\Controllers\Club; -use App\Enums\Mercanet\PaymentResponse; -use App\Enums\Mercanet\RedirectionStatusCode; -use App\Enums\Mercanet\TransactionResponse; use App\Enums\Shop\ContactFormCivility; use App\Enums\Shop\OrderStatus; use App\Enums\Shop\OrderType; @@ -13,7 +10,9 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Club\Shop\ShopBuyRequest; use App\Jobs\Arcadia\SendOrderToArcadia; use App\Jobs\Shop\SendShopMail; -use App\Lib\Mercanet\Mercanet; +use App\Lib\Axepta\AxeptaPayment; +use App\Lib\Axepta\Enums\PaymentResponse; +use App\Lib\Axepta\Objects\AxeptaOrder; use App\Models\DeliveryAddress; use App\Models\Notification; use App\Models\Order; @@ -68,7 +67,6 @@ class ShopController extends Controller try { $order = new Order(); $order->ref = str_replace('-', '', $request->ref); - $order->civility = ContactFormCivility::make(intval($request->coords['civility'])); $order->payementType = OrderType::make(intval($request->payementMethod)); $order->status = OrderStatus::waiting; $order->customer_id = $customerModel->getKey(); @@ -82,13 +80,12 @@ class ShopController extends Controller compact('order') )]); } - // FIXME: uncomment when mercanet cb is ready. - // if ($order->payementType->equals(OrderType::cb)) { - // return response()->json(['redirect' => route( - // 'club.shop.payementCB', - // compact('order') - // )]); - // } + if ($order->payementType->equals(OrderType::cb)) { + return response()->json(['redirect' => route( + 'club.shop.payementCB', + compact('order') + )]); + } abort(500, sprintf('Order already exists but type is unkown %d', $order->getKey())); } //end try @@ -107,11 +104,10 @@ class ShopController extends Controller } switch ($request->payementMethod) { - // FIXME: uncomment when mercanet cb is ready. - // case OrderType::cb->value(): - // \session()->put(sprintf('shop.order.%d.time', $order->getKey()), \time()); - // \session()->save(); - // return response()->json(['redirect' => route('club.shop.payementCB', compact('order'))]); + case OrderType::cb->value(): + \session()->put(sprintf('shop.order.%d.time', $order->getKey()), \time()); + \session()->save(); + return response()->json(['redirect' => route('club.shop.payementCB', compact('order'))]); case OrderType::check->value(): try { \session()->put(sprintf('shop.order.%d.time', $order->getKey()), \time()); @@ -161,7 +157,7 @@ class ShopController extends Controller * * @param \App\Models\Order $order * @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse - * @throws \App\Exceptions\CBPayementError Error response from Mercanet Lib. + * @throws \App\Exceptions\CBPayementError Error response from Axepta Lib. * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */ public function payWithCb(Order $order): \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse @@ -171,14 +167,17 @@ class ShopController extends Controller try { $time = session()->get(sprintf('shop.order.%d.time', $order->getKey()), 0); if ($order->status->equals(OrderStatus::failed)) { - return view('club.pages.shop.error') - ->with('error', trans('Commande annulée, veuillez réessayer')); + return view('club.pages.shop.info') + ->with('title', trans('Commande annulée')) + ->with('message', trans("Le paiement n'a pu aboutir. Veuillez réessayer") + ); } // 4 minutes if (($time + (60 * 4)) < \time()) { - return view('club.pages.shop.error') - ->with('error', trans('Délai d\'attente expiré, veuillez réessayer')); + return view('club.pages.shop.info') + ->with('title', trans('Erreur')) + ->with('message', trans('Délai d\'attente expiré, veuillez réessayer')); } $totalToPay = $order->sellItems->map(function ($sellItem) { @@ -193,39 +192,36 @@ class ShopController extends Controller // * Convert total in cents $totalToPay = (int) (\number_format((float) $totalToPay, 2)) * 100; - $mercanet = $this->getMercanet($order); + $aPayment = new AxeptaPayment( + config('axepta.endpoint'), + config('axepta.merchantId'), + config('axepta.apiKey'), + config('axepta.hmac') + ); + + $aOrder = new AxeptaOrder( + $order->ref, + $totalToPay, + $order->customer->email + ); - $payment = $mercanet->QuickPayment($totalToPay, $order->ref); + try { + $payment = $aPayment->getCheckoutUrl( + $aOrder, + route('club.shop.reglement.payementConfirmCB', $order), + route('club.shop.reglement.payementConfirmCB', ['order' => $order, 'cancel' => true]), + route('club.shop.reglement.payementConfirmCBServ', $order) + ); + return response()->redirectTo($payment); + } catch (\RuntimeException $e) { + $order->status = OrderStatus::failed; + $order->save(); + throw new CBPayementError(sprintf( + "Error response from Axepta Lib : %s", + $e->getMessage() + )); + } - switch ($payment->code) { - case RedirectionStatusCode::redirect->value(): - return view('club.pages.shop.redirectMercanet', [ - 'url' => $payment->url, - 'data' => $payment->data, - 'version' => $payment->version - ]); - case RedirectionStatusCode::referenceAlreadyUsed: - return response()->redirectToRoute('club.shop.index'); - case RedirectionStatusCode::requestError03->value(): - case RedirectionStatusCode::requestError12->value(): - case RedirectionStatusCode::requestError30->value(): - case RedirectionStatusCode::requestError34->value(): - case RedirectionStatusCode::serviceDisponibiltyIssue->value(): - $order->status = OrderStatus::failed; - $order->save(); - throw new CBPayementError(sprintf( - "Error response from Mercanet Lib : %s\n %s", - RedirectionStatusCode::make(intval($payment->code))->label(), - json_encode($payment) - )); - default: - $order->status = OrderStatus::failed; - $order->save(); - throw new CBPayementError(sprintf( - 'Un handled response from Mercanet Lib %s', - json_encode($payment) - )); - } //end switch } catch (ModelNotFoundException | CBPayementError $e) { report($e); Log::critical(sprintf( @@ -233,14 +229,16 @@ class ShopController extends Controller $order->getKey(), $e->getMessage() )); - return view('club.pages.shop.error') - ->with('error', trans('La commande que vous venez de passer n\'a pu aboutir, veuillez réessayer')); + return view('club.pages.shop.info') + ->with('title', trans('Commande annulée')) + ->with('message', trans("Le paiement n'a pu aboutir. Veuillez réessayer") + ); } //end try } /** - * Pas de validate ou custom request sur la réponse de mercanet, - * c'est la Lib mercanet qui fait. + * Pas de validate ou custom request sur la réponse de Axepta, + * C'est la Lib Axepta qui fait. * * @param \Illuminate\Http\Request $request * @param \App\Models\Order $order @@ -248,40 +246,56 @@ class ShopController extends Controller * @throws \App\Exceptions\CBPayementError Data were altered from response. * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */ - public function confirmCbFromMercanet( + public function confirmCbFromAxepta( Request $request, Order $order ): \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory { // phpcs:enable try { - $data = $this->getMercanetData($request); + $aPayment = new AxeptaPayment( + config('axepta.endpoint'), + config('axepta.merchantId'), + config('axepta.apiKey'), + config('axepta.hmac') + ); - /** @var \App\Models\Order */ - $order = Order::query()->where('ref', $data['ref'])->firstOrFail(); - - $mercanet = $this->getMercanet($order); - $mercanet->setResponse($request->post()); - - if (!$mercanet->isValid()) { + if (!$aPayment->isValidWebhookAccess($request)) { $order->status = OrderStatus::failed; $order->save(); // @phpstan-ignore finally.exitPoint throw new CBPayementError('Data were altered from response'); } + + $response = $aPayment->getPaymentStatusById($request->payId); - switch ($data['responseCode']) { - case TransactionResponse::accepted: - $order->status = OrderStatus::payed; + if (is_null($response)) { + if ( + !$order->confirmMailSent and + !$order->status->equals(OrderStatus::payed) + ) { + $order->status = OrderStatus::failed; $order->save(); - SendOrderToArcadia::dispatch($order); - SendShopMail::dispatch($order); + } + return response('', 200); + } + + switch (intval($response[0])) { + case PaymentResponse::ok->value: + if ( + !$order->confirmMailSent and + !$order->status->equals(OrderStatus::payed) + ) { + $order->status = OrderStatus::payed; + $order->save(); + SendOrderToArcadia::dispatch($order); + SendShopMail::dispatch($order); + } break; - case TransactionResponse::canceled: - case TransactionResponse::expired: - case TransactionResponse::refused: - case TransactionResponse::fraud: - case TransactionResponse::duplicate: - case TransactionResponse::maxReached: + case PaymentResponse::transient->value: + case PaymentResponse::emvinfo->value: + break; + case PaymentResponse::error->value: + case PaymentResponse::fatalerror->value: if ( !$order->confirmMailSent and !$order->status->equals(OrderStatus::payed) @@ -290,17 +304,9 @@ class ShopController extends Controller $order->save(); } break; - case TransactionResponse::technicalIssue: - // @phpstan-ignore finally.exitPoint - throw new CBPayementError(sprintf( - 'Error from Mercanet Server %s on ref %s, %s', - $data['responseCode'], - $data['ref'], - json_encode($data['returnDatas']), - )); default: - // * ERROR - if ( + // * ERROR + if ( !$order->confirmMailSent and !$order->status->equals(OrderStatus::payed) ) { @@ -309,12 +315,12 @@ class ShopController extends Controller } // @phpstan-ignore finally.exitPoint throw new CBPayementError(sprintf( - 'Un handled response from Mercanet Server %s on ref %s, %s', - $data['responseCode'], - $data['ref'], - json_encode($data['returnDatas']), + 'Unhandled response from Axepta Server %s on ref %s', + $response, + $order->ref )); - } //end switch + + } //end switch } catch (ModelNotFoundException | CBPayementError | \Exception $e) { report($e); Log::critical(sprintf( @@ -329,135 +335,63 @@ class ShopController extends Controller } /** - * - * Pas de validate ou custom request sur la réponse de mercanet, - * c'est la Lib mercanet qui fait. + * Confirm CB method * * @param Request $request * @param Order $order * @return \Illuminate\Contracts\View\View * @throws \App\Exceptions\CBPayementError Data were altered from response. - * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */ public function confirmCb(Request $request, Order $order): \Illuminate\Contracts\View\View { - // phpcs:enable Notification::getNotifications(); - try { - if ($order->status->equals(OrderStatus::failed)) { - return view('club.pages.shop.error'); - } - if ($order->status->equals(OrderStatus::payed)) { - return view('club.pages.shop.validated'); - } - $mercanet = $this->getMercanet($order); - $mercanet->setResponse($request->post()); - - if (!$mercanet->isValid()) { - try { - $order->status = OrderStatus::failed; - $order->saveOrFail(); - } finally { - throw new CBPayementError("Data were altered from response"); - } - } - - $data = $this->getMercanetData($request); - - switch ($data['responseCode']) { - case PaymentResponse::ok: - try { - $status = $order->status; - $order->status = OrderStatus::payed; - $order->saveOrFail(); - - if ($status->equals(OrderStatus::waiting)) { - SendOrderToArcadia::dispatch($order); - SendShopMail::dispatch($order); - } - } catch (Exception $e) { - report($e); - Log::critical(sprintf( - 'Could not end customer transaction (save and send mail) in boutique for order %d, %s', - $order->getKey(), - $e->getMessage() - )); - } finally { - return view('club.pages.shop.validated'); - } - case PaymentResponse::canceled: - $order->status = OrderStatus::failed; - $order->saveOrFail(); - return view('club.pages.shop.error'); - case PaymentResponse::invalidIds: - case PaymentResponse::invalidRequestFormat: - case PaymentResponse::invalidParameters: - case PaymentResponse::unreachableService: - case PaymentResponse::securityIssue: - case PaymentResponse::transactionAlreadyExists: - case PaymentResponse::expired: - case PaymentResponse::internalIssue: - $order->status = OrderStatus::failed; - $order->saveOrFail(); - throw new CBPayementError(sprintf( - "Error response from Mercanet Lib : %s\n %s", - PaymentResponse::make($data['responseCode'])->label(), - json_encode($data['returnDatas']) - )); - default: - $order->status = OrderStatus::failed; - $order->save(); - throw new CBPayementError(sprintf( - 'Un handled response from Mercanet Lib %s', - json_encode($data['returnDatas']) - )); - } //end switch - } catch (ModelNotFoundException | CBPayementError $e) { - report($e); - Log::critical(sprintf( - 'Could not end customer transaction in boutique for order %d, %s', - $order->getKey(), - $e->getMessage() - )); - return view('club.pages.shop.error'); - } //end try - } - - /** - * Get initialized Mercanet object. - * - * @param \App\Models\Order $order - * @return \App\Lib\Mercanet\Mercanet - */ - private function getMercanet(Order $order): \App\Lib\Mercanet\Mercanet - { - return new Mercanet( - config('mercanet.secret'), - config('mercanet.merchantId'), - config('mercanet.env'), - route('club.shop.reglement.payementConfirmCB', $order), - route('club.shop.reglement.payementConfirmCBServ', $order) - ); - } - - /** - * Get Mercanet ref and responseCode for request. - * - * @param \Illuminate\Http\Request $request - * @return array array['ref' => 'value', 'responseCode' => 'value', 'returnDatas' => 'value'] - */ - private function getMercanetData(Request $request): array - { - $formDatas = []; - $returnDatas = explode("|", $request->post('Data')); - foreach ($returnDatas as $item) { - $tmp = explode("=", $item); - $formDatas[$tmp[0]] = $tmp[1]; + if ( + !$request->has('PayID') or + $request->has('cancel') or + $order->status->equals(OrderStatus::failed) + ) { + return view('club.pages.shop.info') + ->with('title', trans('Commande annulée')) + ->with('message', trans("Le paiement n'a pu aboutir. Veuillez réessayer") + ); } - $ref = $formDatas['transactionReference']; - $responseCode = $formDatas['responseCode']; - return compact('ref', 'responseCode', 'returnDatas'); + if ($order->status->equals(OrderStatus::payed)) { + return view('club.pages.shop.validated'); + } + + $aPayment = new AxeptaPayment( + config('axepta.endpoint'), + config('axepta.merchantId'), + config('axepta.apiKey'), + config('axepta.hmac') + ); + + $response = $aPayment->getPaymentStatusById($request->PayID); + + if (is_null($response)) { + return view('club.pages.shop.info') + ->with('title', trans('Commande annulée')) + ->with('message', trans("Le paiement n'a pu aboutir. Veuillez réessayer") + ); + } + + switch (intval($response[0])) { + case PaymentResponse::ok->value: + return view('club.pages.shop.validated'); + case PaymentResponse::transient->value: + case PaymentResponse::emvinfo->value: + return view('club.pages.shop.info') + ->with('title', 'Paiement en cours') + ->with('message', 'Paiement en attente de traitement par la banque'); + case PaymentResponse::error->value: + case PaymentResponse::fatalerror->value: + default: + return view('club.pages.shop.info') + ->with('title', trans('Commande annulée')) + ->with('message', trans("Le paiement n'a pu aboutir. Veuillez réessayer") + ); + } } } diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index d970e554..6ce1d718 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -87,7 +87,7 @@ class SecureHeaders 'https://use.fontawesome.com', 'https://fonts.googleapis.com', // * VITE DEV - config('app.debug') ? "https://{$host}:4443" : '', + config('app.debug') ? "https://{$host}:4443" : '' ]) . ';', // ! If you dont use ckeditor use this // "style-src 'self' 'nonce-$nonce';", diff --git a/app/Http/Requests/Club/Shop/ShopBuyRequest.php b/app/Http/Requests/Club/Shop/ShopBuyRequest.php index 2b7b2f21..6432bd2c 100644 --- a/app/Http/Requests/Club/Shop/ShopBuyRequest.php +++ b/app/Http/Requests/Club/Shop/ShopBuyRequest.php @@ -29,7 +29,6 @@ class ShopBuyRequest extends FormRequest return [ 'ref' => 'required|string|uuid|unique:orders,ref', 'coords' => 'required|array', - 'coords.civility' => ['required', new Enum(ContactFormCivility::class)], 'coords.firstname' => 'required|string|between:3,255', 'coords.lastname' => 'required|string|between:3,255', 'coords.address' => 'required|string|between:3,255', @@ -38,7 +37,6 @@ class ShopBuyRequest extends FormRequest 'coords.phone' => 'nullable|phone:FR|max:255', 'coords.email' => 'required|email:rfc,strict,dns,spoof,filter', 'shipping' => 'sometimes|array', - 'shipping.civility' => ['required_with:shipping', new Enum(ContactFormCivility::class)], 'shipping.firstname' => 'required_with:shipping|string|between:3,255', 'shipping.lastname' => 'required_with:shipping|string|between:3,255', 'shipping.address' => 'required_with:shipping|string|between:3,255', @@ -62,7 +60,6 @@ class ShopBuyRequest extends FormRequest { return [ 'coords' => trans('vos coordonnées'), - 'coords.civility' => trans('civilité dans vos coordonnées'), 'coords.firstname' => trans('prénom dans vos coordonnées'), 'coords.lastname' => trans('nom dans vos coordonnées'), 'coords.address' => trans('adresse dans vos coordonnées'), @@ -71,7 +68,6 @@ class ShopBuyRequest extends FormRequest 'coords.city' => trans('ville dans vos coordonnées'), 'coords.email' => trans('e-mail dans vos coordonnées'), 'shipping' => trans('votre adresse de livraison'), - 'shipping.civility' => trans('civilité dans votre adresse de livraison'), 'shipping.firstname' => trans('prénom dans votre adresse de livraison'), 'shipping.lastname' => trans('nom dans votre adresse de livraison'), 'shipping.address' => trans('adresse dans votre adresse de livraison'), diff --git a/app/Jobs/DeleteOldMessages.php b/app/Jobs/DeleteOldMessages.php index 1f0c0940..76360b62 100644 --- a/app/Jobs/DeleteOldMessages.php +++ b/app/Jobs/DeleteOldMessages.php @@ -28,7 +28,7 @@ class DeleteOldMessages implements ShouldQueue { Log::debug(sprintf( 'Deleted %d old messages that are more than 30 days', - Message::query()->getMessagesOlderThan(30)->delete() + Message::getMessagesOlderThan(30)->delete() )); Log::debug(sprintf( diff --git a/app/Lib/Arcadia/HasOrderedAnItemData.php b/app/Lib/Arcadia/HasOrderedAnItemData.php index f3eb07c6..4bf0085f 100644 --- a/app/Lib/Arcadia/HasOrderedAnItemData.php +++ b/app/Lib/Arcadia/HasOrderedAnItemData.php @@ -3,6 +3,7 @@ namespace App\Lib\Arcadia; use App\Enums\Arcadia\ArcadiaOrderType; +use App\Enums\Shop\OrderType; use App\Models\Order; use App\Models\SellItem; use Illuminate\Contracts\Support\Arrayable; @@ -103,10 +104,7 @@ class HasOrderedAnItemData implements Jsonable, Arrayable 'label_type_support' => $sellItem->name, 'order_date' => $order->created_at, 'order_num' => $order->getKey(), - 'state_order' => ArcadiaOrderType::check->value(), - // FIXME: uncomment when mercanet cb is ready. - // 'state_order' => $order->payementType->equals(OrderType::cb) ? - // ArcadiaOrderType::cb->value() : ArcadiaOrderType::check->value(), + 'state_order' => $order->payementType->equals(OrderType::cb) ? ArcadiaOrderType::cb->value() : ArcadiaOrderType::check->value(), 'package_unit_weight' => $sellItem->arcadia_weight, 'email' => $order->email, 'firstname' => $firstname, diff --git a/app/Lib/Axepta/AxeptaPayment.php b/app/Lib/Axepta/AxeptaPayment.php new file mode 100644 index 00000000..e67a51aa --- /dev/null +++ b/app/Lib/Axepta/AxeptaPayment.php @@ -0,0 +1,301 @@ +endpoint = $endpoint; + $this->merchantId = $merchantId; + $this->apiKey = $apiKey; + $this->hmac = $hmac; + $this->client = new Client([ + 'base_uri' => $this->endpoint, + 'timeout' => 5.0, + ]); + $this->axeptaCheckoutConfig = [ + ...[ + "language" => "fr", + "amount" => [ + "currency" => "EUR", + ], + "captureMethod" => [ + "type" => "AUTOMATIC" + ], + "allowedPaymentMethods" => [ + "CARD" + ], + "paymentMethods" => [ + "card" => [ + "subType" => ["VISA", "MASTERCARD"], + ], + ] + ], + ...$axeptaConfig + ]; + } + + /** + * getAccessToken method + * https://axeptabnpparibas-docs.redocly.app/authentication/authorizationservice + * @return string + * @throws \RuntimeException + */ + private function getAccessToken(): string + { + if (Cache::has($this->cacheKey)) { + return Cache::get($this->cacheKey); + } + + try { + $response = $this->client->post('/authorization/oauth/token', [ + 'form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->merchantId, + 'client_secret' => $this->apiKey + ] + ]); + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException('Token request failed'); + } + + $responseBody = json_decode($response->getBody()->getContents()); + + if (json_last_error() !== JSON_ERROR_NONE || !isset($responseBody->access_token) || !isset($responseBody->expires_in)) { + throw new \RuntimeException('Invalid token response'); + } + + Cache::put($this->cacheKey, $responseBody->access_token, $responseBody->expires_in); + + return $responseBody->access_token; + } catch (RequestException $e) { + $errorBody = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : '{}'; + $errorData = json_decode($errorBody, true); + $message = $errorData['error'] ?? $errorData['responseDescription'] ?? $e->getMessage(); + throw new \RuntimeException("Axepta token error: {$message}", 0, $e); + } + } + + /** + * getCheckoutUrl method + * + * @param \App\Lib\Axepta\Objects\AxeptaOrder $order + * @param string $returnUrl + * @param string $cancelUrl + * @return string + * @throws \RuntimeException + */ + public function getCheckoutUrl(AxeptaOrder $order, string $returnUrl, string $cancelUrl, string $webhookUrl): string + { + $payload = [ + "transId" => $this->generateTransId(), + "urls" => [ + "return" => $returnUrl, + "cancel" => $cancelUrl, + "webhook" => $webhookUrl + ], + "amount" => [ + "value" => $order->amount + ], + "order" => [ + "merchantReference" => $order->reference, + "creationDate" => gmdate('c'), // ISO 8601 now + ], + "customerInfo" => [ + "email" => $order->customerEmail, + ] + ]; + + try { + $response = $this->client->post('/api/v2/payments/sessions', [ + 'headers' => [ + 'Authorization' => sprintf( + "Bearer %s", + $this->getAccessToken() + ), + 'Content-Type' => 'application/json', + 'Idempotency-Key' => (string) Str::uuid() + ], + 'json' => array_merge_recursive($payload, $this->axeptaCheckoutConfig) // Automatically JSON encodes and sets Content-Type + ]); + + if ($response->getStatusCode() !== 202) { + throw new \RuntimeException('Session creation failed'); + } + + $responseBody = json_decode($response->getBody()->getContents()); + + if (!isset($responseBody->_links->redirect->href)) { + throw new \RuntimeException('Missing redirect URL in response'); + } + + return $responseBody->_links->redirect->href; + } catch (RequestException $e) { + throw new \RuntimeException("Payment session error: " . $e->getMessage(), 0, $e); + } + } + + /** + * generateTransId method + * + * @param int $length + * @return string + */ + private function generateTransId(int $length = 64): string + { + $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?'; + $bytes = random_bytes($length); + $result = ''; + for ($i = 0; $i < $length; $i++) { + $result .= $chars[ord($bytes[$i]) % strlen($chars)]; + } + return $result; + } + + public function getPaymentStatusById(string $paymentId): string|null + { + try { + $response = $this->client->get( + sprintf( + '/api/v2/payments/getByPayId/%s', + $paymentId + ), + [ + 'headers' => [ + 'Authorization' => sprintf( + "Bearer %s", + $this->getAccessToken() + ), + 'Content-Type' => 'application/json', + ] + ] + ); + + $responseBody = json_decode($response->getBody()->getContents()); + return $responseBody->responseCode; + } catch (RequestException $e) { + if ($e->hasResponse()) { + $response = $e->getResponse(); + $responseBody = json_decode($response->getBody()->getContents()); + return $responseBody->responseCode; + } else { + return null; + } + } + } + + /** + * Check signature version + * + * https://axepta.computop.com/display/DOCBNP/Gestion+des+callbacks+et+notification+de+paiement + * + * @param string|null $version + * @return bool + */ + private function isValidSignatureVersion(?string $version): bool + { + return $version === 'v1'; + } + + /** + * Check signature + * + * https://axepta.computop.com/display/DOCBNP/Gestion+des+callbacks+et+notification+de+paiement + * + * @param string $signature + * @param string $timestamp + * @param string $rawPayload + * @return bool + */ + private function isValidSignature(string $signature, string $timestamp, string $rawPayload): bool + { + $providedSignature = $this->extractSignature($signature); + if (!$providedSignature) { + return false; + } + + $signedPayload = "{$timestamp}.{$rawPayload}"; + $expectedSignature = hash_hmac('sha256', $signedPayload, $this->hmac); + + return hash_equals($expectedSignature, $providedSignature); + } + + /** + * Extract the hex value after "v1=" from the signature header. + * + * https://axepta.computop.com/display/DOCBNP/Gestion+des+callbacks+et+notification+de+paiement + * + * @param string $signature + * @return string|null + */ + private function extractSignature(string $signature): ?string + { + if (!str_starts_with($signature, 'v1=')) { + return null; + } + + $hex = substr($signature, 3); + return strlen($hex) === 64 ? $hex : null; // SHA256 hex = 64 chars + } + + /** + * Check if is valid webhook access + * + * From Axepta 2.0 Doc + * https://axepta.computop.com/display/DOCBNP/Gestion+des+callbacks+et+notification+de+paiement + * + * X-Paygate-Signature-Version Signature format version (currently v1) + * X-Paygate-Timestamp Unix epoch timestamp (seconds since 1970-01-01T00:00:00Z, UTC), the timestamp is within ±5 minutes of your local clock. + * X-Paygate-Signature v1= + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function isValidWebhookAccess(\Illuminate\Http\Request $request): bool + { + $signatureVersion = $request->header('x-paygate-signature-version'); + $timestamp = $request->header('x-paygate-timestamp', '0'); + $signature = $request->header('x-paygate-signature'); + + return $this->isValidSignatureVersion($signatureVersion) and + Carbon::createFromTimestampUTC($timestamp)->diffInMinutes(Carbon::now('UTC')) <= 5 and + $this->isValidSignature($signature, $timestamp, $request->getContent()); + } +} diff --git a/app/Lib/Axepta/Enums/PaymentResponse.php b/app/Lib/Axepta/Enums/PaymentResponse.php new file mode 100644 index 00000000..b530b2d7 --- /dev/null +++ b/app/Lib/Axepta/Enums/PaymentResponse.php @@ -0,0 +1,32 @@ +name => 'Transaction successful, other digits may indicate further information', + self::error->name => 'Transaction failed, other digits may indicate further information', + self::fatalerror->name => 'Transaction failed, other digits may indicate further information, e.g. mandatory parameter is missing', + self::transient->name => 'Transaction pending. Final status will be sent asynchronously when available', + self::emvinfo->name => 'Intermediate states in the EMV 3-D Secure sequence' + ]; + //phpcs:enable +} diff --git a/app/Lib/Axepta/Objects/AxeptaOrder.php b/app/Lib/Axepta/Objects/AxeptaOrder.php new file mode 100644 index 00000000..0e96446f --- /dev/null +++ b/app/Lib/Axepta/Objects/AxeptaOrder.php @@ -0,0 +1,20 @@ + */ - private $brandsmap = [ - 'ACCEPTGIRO' => 'CREDIT_TRANSFER', - 'AMEX' => 'CARD', - 'BCMC' => 'CARD', - 'BUYSTER' => 'CARD', - 'BANK CARD' => 'CARD', - 'CB' => 'CARD', - 'IDEAL' => 'CREDIT_TRANSFER', - 'INCASSO' => 'DIRECT_DEBIT', - 'MAESTRO' => 'CARD', - 'MASTERCARD' => 'CARD', - 'MASTERPASS' => 'CARD', - 'MINITIX' => 'OTHER', - 'NETBANKING' => 'CREDIT_TRANSFER', - 'PAYPAL' => 'CARD', - 'PAYLIB' => 'CARD', - 'REFUND' => 'OTHER', - 'SDD' => 'DIRECT_DEBIT', - 'SOFORT' => 'CREDIT_TRANSFER', - 'VISA' => 'CARD', - 'VPAY' => 'CARD', - 'VISA ELECTRON' => 'CARD', - 'CBCONLINE' => 'CREDIT_TRANSFER', - 'KBCONLINE' => 'CREDIT_TRANSFER' - ]; - - /** @var string $secretKey */ - private $secretKey; - - /** @var string $env */ - private $env; - - /** @var string $returnUrl */ - private $returnUrl; - - /** @var boolean|string */ - private $automaticUrl = false; - - /** @var string $pspURL */ - private $pspURL = self::DEVELOPMENT; - - /** @var string */ - private $responseData; - - /** @var array */ - private $parameters = []; - - /** @var array */ - private $pspFields = [ - 'amount', - 'cardExpiryDate', - 'cardNumber', - 'cardCSCValue', - 'currencyCode', - 'merchantId', - 'interfaceVersion', - 'sealAlgorithm', - 'transactionReference', - 'keyVersion', - 'paymentMeanBrand', - 'customerLanguage', - 'billingAddress.city', - 'billingAddress.company', - 'billingAddress.country', - 'billingAddress', - 'billingAddress.postBox', - 'billingAddress.state', - 'billingAddress.street', - 'billingAddress.streetNumber', - 'billingAddress.zipCode', - 'billingContact.email', - 'billingContact.firstname', - 'billingContact.gender', - 'billingContact.lastname', - 'billingContact.mobile', - 'billingContact.phone', - 'customerAddress', - 'customerAddress.city', - 'customerAddress.company', - 'customerAddress.country', - 'customerAddress.postBox', - 'customerAddress.state', - 'customerAddress.street', - 'customerAddress.streetNumber', - 'customerAddress.zipCode', - 'customerEmail', - 'customerContact', - 'customerContact.email', - 'customerContact.firstname', - 'customerContact.gender', - 'customerContact.lastname', - 'customerContact.mobile', - 'customerContact.phone', - 'customerContact.title', - 'expirationDate', - 'automaticResponseUrl', - 'templateName', - 'paymentMeanBrandList', - 'instalmentData.number', - 'instalmentData.datesList', - 'instalmentData.transactionReferencesList', - 'instalmentData.amountsList', - 'paymentPattern', - 'captureDay', - 'captureMode', - 'merchantTransactionDateTime', - 'fraudData.bypass3DS', - 'seal', - 'orderChannel', - 'orderId', - 'returnContext', - 'transactionOrigin', - 'merchantWalletId', - 'paymentMeanId' - ]; - - /** @var array */ - private $requiredFields = [ - 'amount', - 'currencyCode', - 'interfaceVersion', - 'keyVersion', - 'merchantId', - 'normalReturnUrl', - 'orderChannel', - 'transactionReference' - ]; - - /** @var array */ - public $allowedlanguages = [ - 'nl', - 'fr', - 'de', - 'it', - 'es', - 'cy', - 'en' - ]; - - /** @var array */ - private static $currencies = [ - 'EUR' => '978', - 'USD' => '840', - 'CHF' => '756', - 'GBP' => '826', - 'CAD' => '124', - 'JPY' => '392', - 'MXP' => '484', - 'TRY' => '949', - 'AUD' => '036', - 'NZD' => '554', - 'NOK' => '578', - 'BRC' => '986', - 'ARP' => '032', - 'KHR' => '116', - 'TWD' => '901', - 'SEK' => '752', - 'DKK' => '208', - 'KRW' => '410', - 'SGD' => '702', - 'XPF' => '953', - 'XOF' => '952' - ]; - - /** - * @param string $secret - * @param string $MerchantId - * @param string $env - * @param string $returnUrl - * @param string|false $automaticUrl - */ - public function __construct( - string $secret, - string $MerchantId, - string $env, - string $returnUrl, - string|false $automaticUrl = false - ) { - $this->secretKey = $secret; - $this->parameters['merchantId'] = $MerchantId; - $this->env = $env; - $this->returnUrl = $returnUrl; - if ($automaticUrl !== false) { - $this->automaticUrl = $automaticUrl; - } - - switch ($this->env) { - case 'DEVELOPMENT': - $this->setUrl(Mercanet::DEVELOPMENT); - break; - case 'PRODUCTION': - $this->setUrl(Mercanet::PRODUCTION); - break; - } - } - - /** - * Convert currency to currency code. - * - * @param string $currency - * @return string - * @throws \InvalidArgumentException Unknown currencyCode. - */ - public static function convertCurrencyToCurrencyCode(string $currency): string - { - if (!in_array($currency, array_keys(self::$currencies))) { - throw new InvalidArgumentException("Unknown currencyCode $currency."); - } - return self::$currencies[$currency]; - } - - /** - * Convert currency code to currency. - * - * @param string $code - * @return integer|string|false - * @throws \InvalidArgumentException Unknown Code. - */ - public static function convertCurrencyCodeToCurrency(string $code): int|string|false - { - if (!in_array($code, array_values(self::$currencies))) { - throw new InvalidArgumentException("Unknown Code $code."); - } - return array_search($code, self::$currencies); - } - - /** - * Get currencies. - * - * @return array - */ - public static function getCurrencies(): array - { - return self::$currencies; - } - - /** - * Sha compose. - * - * @param array $parameters - * @return string - */ - public function shaCompose(array $parameters): string - { - $shaString = ''; - foreach ($parameters as $key => $value) { - if ($key != 'keyVersion' && $key != 'sealAlgorithm') { - $shaString .= (is_array($value)) ? implode('', $value) : $value; - } - } - return $shaString; - } - - /** - * Get sha sign. - * - * @return string - */ - public function getShaSign(): string - { - $this->validate(); - // FIXME: deprecated in 8.2, gonna be deleted in 9.0 - return hash_hmac('sha256', utf8_encode($this->shaCompose($this->toArray())), $this->secretKey); - } - - /** - * Get url. - * - * @return string - */ - public function getUrl(): string - { - return $this->pspURL; - } - - /** - * Set url. - * - * @param string $pspUrl - * @return void - */ - public function setUrl(string $pspUrl): void - { - $this->validateUri($pspUrl); - $this->pspURL = $pspUrl; - } - - /** - * Set normal return url. - * - * @param string $url - * @return void - */ - public function setNormalReturnUrl(string $url): void - { - $this->validateUri($url); - $this->parameters['normalReturnUrl'] = $url; - } - - /** - * Set automatic response url. - * - * @param string $url - * @return void - */ - public function setAutomaticResponseUrl(string $url): void - { - $this->validateUri($url); - $this->parameters['automaticResponseUrl'] = $url; - } - - /** - * Set automatic response url. - * - * @param string $transactionReference - * @return void - * @throws \InvalidArgumentException TransactionReference cannot contain special characters. - */ - public function setTransactionReference(string $transactionReference): void - { - if (preg_match('/[^a-zA-Z0-9_-]/', $transactionReference)) { - throw new \InvalidArgumentException("TransactionReference cannot contain special characters"); - } - $this->parameters['transactionReference'] = $transactionReference; - } - - /** - * Set amount in cents, eg EUR 12.34 is written as 1234. - * - * @param mixed $amount - * @return void - * @throws \InvalidArgumentException Integer expected. Amount is always in cents - * OR Amount must be a positive number. - */ - public function setAmount(mixed $amount): void - { - if (!is_int($amount)) { - throw new InvalidArgumentException("Integer expected. Amount is always in cents"); - } - if ($amount <= 0) { - throw new InvalidArgumentException("Amount must be a positive number"); - } - $this->parameters['amount'] = "$amount"; - } - - /** - * Set currency. - * - * @param string $currency - * @return void - * @throws \InvalidArgumentException Unknown currency. - */ - public function setCurrency(string $currency): void - { - if (!array_key_exists(strtoupper($currency), self::getCurrencies())) { - throw new InvalidArgumentException("Unknown currency"); - } - $this->parameters['currencyCode'] = self::convertCurrencyToCurrencyCode($currency); - } - - /** - * Set language. - * - * @param string $language - * @return void - * @throws \InvalidArgumentException Invalid language locale. - */ - public function setLanguage(string $language): void - { - if (!in_array($language, $this->allowedlanguages)) { - throw new InvalidArgumentException("Invalid language locale"); - } - $this->parameters['customerLanguage'] = $language; - } - - /** - * Set customer email. - * - * @param string $email - * @return void - */ - public function setCustomerEmail(string $email): void - { - $this->parameters['customerEmail'] = $email; - } - - /** - * Set payment brands. - * - * @param array $brands - * @return void - * @throws \InvalidArgumentException Unknown brand. - */ - public function setPaymentBrands(array $brands): void - { - $this->parameters['paymentMeanBrandList'] = ''; - foreach ($brands as $brand) { - if (!array_key_exists(strtoupper($brand), $this->brandsmap)) { - throw new InvalidArgumentException("Unknown Brand [$brand]."); - } - } - $this->parameters['paymentMeanBrandList'] = $brands; - } - - /** - * Set billing contacte email. - * - * @param string $email - * @return void - * @throws \InvalidArgumentException Email is too long OR email is invalid. - */ - public function setBillingContactEmail(string $email): void - { - if (strlen($email) > 50) { - throw new InvalidArgumentException("Email is too long"); - } - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException("Email is invalid"); - } - $this->parameters['billingContact.email'] = $email; - } - - /** - * Set billing address street. - * - * @param string $street - * @return void - * @throws \InvalidArgumentException Street is too long. - */ - public function setBillingAddressStreet(string $street): void - { - if (strlen($street) > 35) { - throw new InvalidArgumentException("street is too long"); - } - $this->parameters['billingAddress.street'] = Normalizer::normalize($street); - } - - /** - * Set billing address street number. - * - * @param string $nr - * @return void - * @throws \InvalidArgumentException StreetNumber is too long. - */ - public function setBillingAddressStreetNumber(string $nr): void - { - if (strlen($nr) > 10) { - throw new InvalidArgumentException("streetNumber is too long"); - } - $this->parameters['billingAddress.streetNumber'] = Normalizer::normalize($nr); - } - - /** - * Set billing address zip code. - * - * @param string $zipCode - * @return void - * @throws \InvalidArgumentException ZipCode is too long. - */ - public function setBillingAddressZipCode(string $zipCode): void - { - if (strlen($zipCode) > 10) { - throw new InvalidArgumentException("zipCode is too long"); - } - $this->parameters['billingAddress.zipCode'] = Normalizer::normalize($zipCode); - } - - /** - * Set billing address city. - * - * @param string $city - * @return void - * @throws \InvalidArgumentException City is too long. - */ - public function setBillingAddressCity(string $city): void - { - if (strlen($city) > 25) { - throw new InvalidArgumentException("city is too long"); - } - $this->parameters['billingAddress.city'] = Normalizer::normalize($city); - } - - /** - * Set billing contact phone. - * - * @param string $phone - * @return void - * @throws \InvalidArgumentException Phone is too long. - */ - public function setBillingContactPhone(string $phone): void - { - if (strlen($phone) > 30) { - throw new InvalidArgumentException("phone is too long"); - } - $this->parameters['billingContact.phone'] = $phone; - } - - /** - * Set billing contact firstname. - * - * @param string $firstname - * @return void - */ - public function setBillingContactFirstname(string $firstname): void - { - // Replace quotes. - $this->parameters['billingContact.firstname'] = str_replace( - array("'", '"'), - '', - Normalizer::normalize($firstname) - ); - } - - /** - * Set billing contact lastname. - * - * @param string $lastname - * @return void - */ - public function setBillingContactLastname(string $lastname): void - { - // Replace quotes. - $this->parameters['billingContact.lastname'] = str_replace( - array("'", '"'), - '', - Normalizer::normalize($lastname) - ); - } - - /** - * Set capture day. - * - * @param string $number - * @return void - * @throws \InvalidArgumentException CaptureDay is too long. - */ - public function setCaptureDay(string $number): void - { - if (strlen($number) > 2) { - throw new InvalidArgumentException("captureDay is too long"); - } - $this->parameters['captureDay'] = $number; - } - - /** - * Set capture mode. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException CaptureMode is too long. - */ - public function setCaptureMode(string $value): void - { - if (strlen($value) > 20) { - throw new InvalidArgumentException("captureMode is too long"); - } - $this->parameters['captureMode'] = $value; - } - - /** - * Set merchant transaction date time. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException MerchantTransactionDateTime is too long. - */ - public function setMerchantTransactionDateTime(string $value): void - { - if (strlen($value) > 25) { - throw new InvalidArgumentException("merchantTransactionDateTime is too long"); - } - $this->parameters['merchantTransactionDateTime'] = $value; - } - - /** - * Set interface version. - * - * @param string $value - * @return void - */ - public function setInterfaceVersion(string $value): void - { - $this->parameters['interfaceVersion'] = $value; - } - - /** - * setkeyVersion method - * - * @param string $version - * @return void - */ - public function setKeyVersion(string $version): void - { - $this->parameters['keyVersion'] = $version; - } - - /** - * Set seal algorithm. - * - * @param string $value - * @return void - */ - public function setSealAlgorithm(string $value): void - { - $this->parameters['sealAlgorithm'] = $value; - } - - /** - * Set seal algorithm. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException OrderChannel is too long. - */ - public function setOrderChannel(string $value): void - { - if (strlen($value) > 20) { - throw new InvalidArgumentException("orderChannel is too long"); - } - $this->parameters['orderChannel'] = $value; - } - - /** - * Set order id. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException OrderId is too long. - */ - public function setOrderId(string $value): void - { - if (strlen($value) > 32) { - throw new InvalidArgumentException("orderId is too long"); - } - $this->parameters['orderId'] = $value; - } - - /** - * Set return context. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException ReturnContext is too long. - */ - public function setReturnContext(string $value): void - { - if (strlen($value) > 255) { - throw new InvalidArgumentException("returnContext is too long"); - } - $this->parameters['returnContext'] = $value; - } - - /** - * Set transaction origin. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException TransactionOrigin is too long. - */ - public function setTransactionOrigin(string $value): void - { - if (strlen($value) > 20) { - throw new InvalidArgumentException("transactionOrigin is too long"); - } - $this->parameters['transactionOrigin'] = $value; - } - - /** - * Set card number. - * - * @param string $number - * @return void - * @throws \InvalidArgumentException CardNumber is too long OR cardNumber is too short. - */ - public function setCardNumber(string $number): void - { - if (strlen($number) > 19) { - throw new InvalidArgumentException("cardNumber is too long"); - } - if (strlen($number) < 12) { - throw new InvalidArgumentException("cardNumber is too short"); - } - $this->parameters['cardNumber'] = $number; - } - - /** - * Set card expiry date. - * - * @param string $date - * @return void - * @throws \InvalidArgumentException CardExpiryDate value is invalid. - */ - public function setCardExpiryDate(string $date): void - { - if (strlen($date) != 6) { - throw new InvalidArgumentException("cardExpiryDate value is invalid"); - } - $this->parameters['cardExpiryDate'] = $date; - } - - /** - * Set card csc value. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException CardCSCValue value is invalid. - */ - public function setCardCscValue(string $value): void - { - if (strlen($value) > 4) { - throw new InvalidArgumentException("cardCSCValue value is invalid"); - } - $this->parameters['cardCSCValue'] = $value; - } - - /** - * Set fraud data bypass3ds. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException FraudData.bypass3DS is too long. - */ - public function setFraudDataBypass3ds(string $value): void - { - if (strlen($value) > 128) { - throw new InvalidArgumentException("fraudData.bypass3DS is too long"); - } - $this->parameters['fraudData.bypass3DS'] = $value; - } - - /** - * Set merchant wallet id. - * - * @param string $wallet - * @return void - * @throws \InvalidArgumentException MerchantWalletId is too long. - */ - public function setMerchantWalletId(string $wallet): void - { - if (strlen($wallet) > 21) { - throw new InvalidArgumentException("merchantWalletId is too long"); - } - $this->parameters['merchantWalletId'] = $wallet; - } - - /** - * Set payment mean id. - * - * @param string $value - * @return void - * @throws \InvalidArgumentException PaymentMeanId is too long. - */ - public function setPaymentMeanId(string $value): void - { - if (strlen($value) > 6) { - throw new InvalidArgumentException("paymentMeanId is too long"); - } - $this->parameters['paymentMeanId'] = $value; - } - - /** - * Set instalment data number. - * - * @param string $number - * @return void - * @throws \InvalidArgumentException InstalmentData.number is too long OR - * instalmentData.number invalid value : value must be set between 2 and 50. - */ - public function setInstalmentDataNumber(string $number): void - { - if (strlen($number) > 2) { - throw new InvalidArgumentException("instalmentData.number is too long"); - } - if (($number < 2) || ($number > 50)) { - throw new InvalidArgumentException( - "instalmentData.number invalid value : value must be set between 2 and 50" - ); - } - $this->parameters['instalmentData.number'] = $number; - } - - /** - * Set instalment dates list. - * - * @param string $datesList - * @return void - */ - public function setInstalmentDatesList(string $datesList): void - { - $this->parameters['instalmentData.datesList'] = $datesList; - } - - /** - * Set instalment data transaction references list. - * - * @param string $transactionReferencesList - * @return void - */ - public function setInstalmentDataTransactionReferencesList(string $transactionReferencesList): void - { - $this->parameters['instalmentData.transactionReferencesList'] = $transactionReferencesList; - } - - /** - * Set instalment data amounts list. - * - * @param string $amountsList - * @return void - */ - public function setInstalmentDataAmountsList(string $amountsList): void - { - $this->parameters['instalmentData.amountsList'] = $amountsList; - } - - /** - * Set payment pattern. - * - * @param string $paymentPattern - * @return void - */ - public function setPaymentPattern(string $paymentPattern): void - { - $this->parameters['paymentPattern'] = $paymentPattern; - } - - /** - * @param string $method - * @param array $args - * @return mixed - * @throws \BadMethodCallException Unknown method. - */ - public function __call(string $method, array $args) - { - if (substr($method, 0, 3) == 'set') { - $field = lcfirst(substr($method, 3)); - if (in_array($field, $this->pspFields)) { - $this->parameters[$field] = $args[0]; - return; - } - } - - if (substr($method, 0, 3) == 'get') { - $field = lcfirst(substr($method, 3)); - if (array_key_exists($field, $this->parameters)) { - return $this->parameters[$field]; - } - } - - throw new BadMethodCallException("Unknown method $method"); - } - - /** - * Apply sort on parameters. - * - * @return array - */ - public function toArray(): array - { - ksort($this->parameters); - return $this->parameters; - } - - /** - * To parameter string. - * - * @return string|false - */ - public function toParameterString(): string|false - { - ksort($this->parameters); - $seal = $this->getShaSign(); - $tmp = array_merge($this->parameters, ['seal' => $seal]); - return json_encode($tmp, JSON_UNESCAPED_SLASHES); - } - - /** - * Undocumented function - * - * @return void - * @throws \RuntimeException Can not be empty. - */ - public function validate(): void - { - foreach ($this->requiredFields as $field) { - if (empty($this->parameters[$field])) { - throw new \RuntimeException($field . " can not be empty"); - } - } - } - - /** - * Validate uri. - * - * @param string $uri - * @return void - * @throws \InvalidArgumentException Uri is not valid OR Uri is too long. - */ - protected function validateUri(string $uri): void - { - if (!filter_var($uri, FILTER_VALIDATE_URL)) { - throw new InvalidArgumentException("Uri is not valid"); - } - if (strlen($uri) > 200) { - throw new InvalidArgumentException("Uri is too long"); - } - } - - // Traitement des reponses de Mercanet. - - /** @var string */ - public const SHASIGN_FIELD = "SEAL"; - - /** @var string */ - public const DATA_FIELD = "DATA"; - - /** @var string */ - private $shaSign; - - /** @var string */ - private $dataString; - - // FIXME: don't know the type here for $responseRequest. - - /** @var mixed $responseRequest @phpstan-ignore property.onlyRead */ - private $responseRequest; - - // FIXME: don't know the type here for $parameterArray. - - /** @var mixed $responseRequest @phpstan-ignore property.onlyRead */ - private $parameterArray; - - /** - * Set response. - * - * @param array $httpRequest - * @return void - */ - public function setResponse(array $httpRequest): void - { - // Use lowercase internally. - $httpRequest = array_change_key_case($httpRequest, CASE_UPPER); - - // Set sha sign. - $this->shaSign = $this->extractShaSign($httpRequest); - - // Filter request for Sips parameters. - $this->parameters = $this->filterRequestParameters($httpRequest); - } - - /** - * Filter http request parameters. - * - * @param array $httpRequest - * @return array - * @throws \InvalidArgumentException Data parameter not present in parameters. - */ - private function filterRequestParameters(array $httpRequest): array - { - // Filter request for Sips parameters. - if (!array_key_exists(self::DATA_FIELD, $httpRequest) || $httpRequest[self::DATA_FIELD] == '') { - throw new InvalidArgumentException('Data parameter not present in parameters.'); - } - $this->responseData = $httpRequest[self::DATA_FIELD]; - $this->dataString = $httpRequest[self::DATA_FIELD]; - return \json_decode($this->dataString, true); - } - - /** - * Get seal. - * - * @return string - */ - public function getSeal(): string - { - return $this->shaSign; - } - - /** - * Extract sha sign. - * - * @param array $parameters - * @return string - * @throws \InvalidArgumentException SHASIGN parameter not present in parameters. - */ - private function extractShaSign(array $parameters): string - { - if (!array_key_exists(self::SHASIGN_FIELD, $parameters) || $parameters[self::SHASIGN_FIELD] == '') { - throw new InvalidArgumentException('SHASIGN parameter not present in parameters.'); - } - - return $parameters[self::SHASIGN_FIELD]; - } - - /** - * Concat all values from parameters into on sigle string. - * - * @param array $params - * @param string $values - * @return string - */ - public function getValuesFromParameters(array $params = [], string $values = ""): string - { - foreach ($params as $value) { - $values .= !\is_array($value) ? \trim($value) : $this->getValuesFromParameters($value); - } - return $values; - } - - /** - * Checks if the response is valid. - * - * @return boolean - */ - public function isValid(): bool - { - Log::debug("Mercanet Response Seal : " . $this->shaSign); - Log::debug("Mercanet Response : " . $this->responseData); - $resultat = false; - $compute = hash_hmac('sha256', $this->responseData, $this->secretKey); - if (hash_equals($this->shaSign, $compute)) { - if ( - in_array($this->parameters['responseCode'], [ - '00', // Paiement accepté. - '02', // Demande d'autorisation par téléphone à la banque à cause d'un dépassement du plafond - // d'autorisation sur la carte, si vous êtes autorisé à forcer les transactions. - '03', // Contrat commerçant invalide. - '05', // Autorisation refusée Refus 3D Secure, Refus bancaire acquéreur, Repli VADS, Fraude. - '11', // Utilisé dans le cas d'un contrôle différé. Le PAN est en opposition. - '12', // Transaction invalide, vérifier les paramètres transférés dans la requête. - '14', // Coordonnées du moyen de paiement invalides (ex: n° de carte ou cryptogramme visuel de - // la carte) ou vérification AVS échouée. - '17', // Annulation de l'acheteur. - '30', // Erreur de format. - '34', // Suspicion de fraude (seal erroné), Refus fraude acquéreur. - '54', // Date de validité du moyen de paiement dépassée. - '60', // Transaction en attente. - '75', // Nombre de tentatives de saisie des coordonnées du moyen de paiement sous Sips Paypage dépassé. - '90', // Service temporairement indisponible. - '94', // Transaction dupliquée : le transactionReference de la transaction est déjà utilisé. - '97', // Délai expiré, transaction refusée. - '99' // Problème temporaire du serveur de paiement. - ]) - ) { - $resultat = true; - } - } //end if - return $resultat; - } - - /** - * Retrieves a response parameter. - * - * @param string $key - * @return mixed - */ - public function getParam(string $key): mixed - { - return $this->parameterArray[$key]; - } - - /** - * Get response request. - * - * @return \App\Lib\Mercanet\Mercanet - */ - public function getResponseRequest(): \App\Lib\Mercanet\Mercanet - { - return $this->responseRequest; - } - - /** - * Order payment. - * - * @param integer $amount - * @param string $ref - * @return object - */ - public function quickPayment(int $amount, string $ref): object - { - // Seal. - $this->setSealAlgorithm("HMAC-SHA-256"); - // Required fields. - $this->setInterfaceVersion(Mercanet::INTERFACE_VERSION); - $this->setKeyVersion(Mercanet::VERSION); - $this->setCurrency(Mercanet::CURRENCY); - $this->setOrderChannel("INTERNET"); - // Variables. - $this->setAmount($amount); - $this->setTransactionReference($ref); - $this->setNormalReturnUrl($this->returnUrl); - if ($this->automaticUrl !== false) { - $this->setAutomaticResponseUrl($this->automaticUrl); - } - // Optional fields. - $this->setPaymentBrands(Mercanet::PAYMENT_BRANDS); - $this->setCaptureMode('IMMEDIATE'); - // Validate. - $this->validate(); - - return $this->executeRequest(); - } - - /** - * Execute request. - * - * @return object - */ - public function executeRequest(): object - { - Log::debug("Mercanet Request : " . $this->toParameterString()); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->getUrl()); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $this->toParameterString()); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Accept:application/json')); - curl_setopt($ch, CURLOPT_PORT, 443); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - $result = curl_exec($ch); - $info = curl_getinfo($ch); - - if (!$result) { - report(new \RuntimeException( - "MERCANET [" . strval($this->parameters['transactionReference']) . "] : curl error: " . curl_error($ch) - )); - Log::critical( - "MERCANET [" . strval($this->parameters['transactionReference']) . "] : curl error: " . curl_error($ch) - ); - curl_close($ch); - return (object) ['status' => 'KO', 'code' => 500]; - } - - if ($info['http_code'] != 200) { - Log::critical(sprintf( - "MERCANET [%s] : service error: %s - Return : %s", - $this->parameters['transactionReference'], - $info['http_code'], - $result - )); - curl_close($ch); - return (object) ['status' => 'KO', 'code' => 500]; - } - curl_close($ch); - - if (strlen($result) == 0) { - Log::critical(sprintf( - "MERCANET [%s] : service did not sent back data", - $this->parameters['transactionReference'] - )); - return (object) ['status' => 'KO', 'code' => 500]; - } - $result_array = json_decode($result); - if (isset($result_array->redirectionStatusCode)) { - if ($result_array->redirectionStatusCode === '00') { - return (object) [ - 'status' => 'OK', - 'code' => $result_array->redirectionStatusCode, - // @phpstan-ignore property.notFound - 'url' => $result_array->redirectionUrl, - // @phpstan-ignore property.notFound - 'version' => $result_array->redirectionVersion, - // @phpstan-ignore property.notFound - 'data' => $result_array->redirectionData, - ]; - } else { - return (object) [ - 'status' => 'KO', - 'code' => $result_array->redirectionStatusCode, - // @phpstan-ignore property.notFound - 'seal' => $result_array->seal, - ]; - } - } else { - Log::critical(sprintf( - "MERCANET [%s] : redirectionStatusCode missing", - $this->parameters['transactionReference'] - )); - return (object) ['status' => 'KO', 'code' => 500]; - } //end if - } -} diff --git a/app/Models/Message.php b/app/Models/Message.php index 4b046e05..5a47a5e0 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -7,6 +7,7 @@ use App\Models\Notification; use App\Interfaces\Notifiable as INotifiable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; class Message extends Model implements INotifiable { @@ -79,9 +80,9 @@ class Message extends Model implements INotifiable * Get older messages. * * @param integer $days - * @return \Illuminate\Contracts\Database\Eloquent\Builder + * @return Builder */ - public static function getMessagesOlderThan(int $days): \Illuminate\Contracts\Database\Eloquent\Builder + public static function getMessagesOlderThan(int $days): Builder { return self::query()->where('updated_at', '<', now()->subDays($days)); } diff --git a/app/Models/Order.php b/app/Models/Order.php index 35be826c..765a7179 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -67,7 +67,6 @@ class Order extends Model { return [ 'ref' => $this->ref, - 'civility' => $this->civility->value(), 'lastname' => $this->lastname, 'firstname' => $this->firstname, 'address' => $this->address, diff --git a/config/axepta.php b/config/axepta.php new file mode 100644 index 00000000..4d3aaec6 --- /dev/null +++ b/config/axepta.php @@ -0,0 +1,8 @@ + env('AXEPTA_ENDPOINT'), + 'merchantId' => env('AXEPTA_MERCHANT_ID'), + 'apiKey' => env('AXEPTA_APIKEY'), + 'hmac' => env('AXEPTA_HMAC') +]; diff --git a/config/mercanet.php b/config/mercanet.php deleted file mode 100644 index 9864fd26..00000000 --- a/config/mercanet.php +++ /dev/null @@ -1,8 +0,0 @@ - env('MERCANET_SECRET'), - 'merchantId' => env('MERCANET_MERCHAND_ID'), - // Ex: DEVELOPMENT, PRODUCTION. - 'env' => env('MERCANET_ENV', 'DEVELOPMENT'), -]; diff --git a/manifest.yaml b/manifest.yaml index 647efb5b..2082728f 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -58,6 +58,10 @@ data: REDIS_PREFIX: "${APP_REDIS_PREFIX}" REDIS_SCHEME: "${APP_REDIS_SCHEME}" UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}" + AXEPTA_ENDPOINT: "${APP_AXEPTA_ENDPOINT}" + AXEPTA_MERCHANT_ID: "${APP_AXEPTA_MERCHANT_ID}" + AXEPTA_APIKEY: "${APP_AXEPTA_APIKEY}" + AXEPTA_HMAC: "${APP_AXEPTA_HMAC}" --- apiVersion: v1 kind: PersistentVolumeClaim diff --git a/public/newsletters/251201/index.html b/public/newsletters/251201/index.html index b0aa3dd9..a85dd70f 100644 --- a/public/newsletters/251201/index.html +++ b/public/newsletters/251201/index.html @@ -144,7 +144,9 @@ Le Club Le Gaulois Bienvenue sur le club des Gaulois @@ -337,7 +339,8 @@ Logo Le Gaulois Logo marque Le Gaulois @@ -391,6 +394,7 @@ href="https://www.legaulois.fr/club?mtm_campaign=le_gaulois_mailing_consentement_newsletter_oct2025_club_des_gaulois&mtm_source=mailing_consentement_nl_le_gaulois&mtm_medium=mailing_consentement_le_Gaulois" target="_blank"> Page internet Club des Gaulois @@ -420,6 +424,7 @@ href="https://www.linkedin.com/company/legaulois" target="_blank"> Logo Linkedin Le Gaulois @@ -449,6 +454,7 @@ href="https://www.instagram.com/legaulois_officiel/" target="_blank"> Logo Instagram Le Gaulois @@ -478,7 +484,8 @@ href="https://www.tiktok.com/@legaulois_off" target="_blank"> Logo Tiktok Le Gaulois @@ -507,6 +514,7 @@ href="https://www.youtube.com/channel/UCiTpBkZrwH9_xUpTv1UTuLA" target="_blank"> Logo Youtube Le Gaulois @@ -536,6 +544,7 @@ href="https://www.facebook.com/LeGauloisFrance/" target="_blank"> Logo Facebook Le Gaulois diff --git a/public/newsletters/260317/assets/bloc_2_1.png b/public/newsletters/260317/assets/bloc_2_1.png new file mode 100644 index 00000000..f3e4d403 Binary files /dev/null and b/public/newsletters/260317/assets/bloc_2_1.png differ diff --git a/public/newsletters/260317/assets/bloc_2_2.png b/public/newsletters/260317/assets/bloc_2_2.png new file mode 100644 index 00000000..34237bb8 Binary files /dev/null and b/public/newsletters/260317/assets/bloc_2_2.png differ diff --git a/public/newsletters/260317/assets/bloc_2_3.png b/public/newsletters/260317/assets/bloc_2_3.png new file mode 100644 index 00000000..6da58466 Binary files /dev/null and b/public/newsletters/260317/assets/bloc_2_3.png differ diff --git a/public/newsletters/260317/assets/bloc_2_4.png b/public/newsletters/260317/assets/bloc_2_4.png new file mode 100644 index 00000000..4153644a Binary files /dev/null and b/public/newsletters/260317/assets/bloc_2_4.png differ diff --git a/public/newsletters/260317/assets/border_1.png b/public/newsletters/260317/assets/border_1.png new file mode 100644 index 00000000..f73e9934 Binary files /dev/null and b/public/newsletters/260317/assets/border_1.png differ diff --git a/public/newsletters/260317/assets/border_2.png b/public/newsletters/260317/assets/border_2.png new file mode 100644 index 00000000..a6bc1bb3 Binary files /dev/null and b/public/newsletters/260317/assets/border_2.png differ diff --git a/public/newsletters/260317/assets/border_3.png b/public/newsletters/260317/assets/border_3.png new file mode 100644 index 00000000..119d0c9f Binary files /dev/null and b/public/newsletters/260317/assets/border_3.png differ diff --git a/public/newsletters/260317/assets/border_4.png b/public/newsletters/260317/assets/border_4.png new file mode 100644 index 00000000..ce654cf3 Binary files /dev/null and b/public/newsletters/260317/assets/border_4.png differ diff --git a/public/newsletters/260317/assets/border_5.png b/public/newsletters/260317/assets/border_5.png new file mode 100644 index 00000000..1933b8bd Binary files /dev/null and b/public/newsletters/260317/assets/border_5.png differ diff --git a/public/newsletters/260317/assets/chaussettes.png b/public/newsletters/260317/assets/chaussettes.png new file mode 100644 index 00000000..47d2c169 Binary files /dev/null and b/public/newsletters/260317/assets/chaussettes.png differ diff --git a/public/newsletters/260317/assets/cuisses_de_canard.png b/public/newsletters/260317/assets/cuisses_de_canard.png new file mode 100644 index 00000000..2acdb2c3 Binary files /dev/null and b/public/newsletters/260317/assets/cuisses_de_canard.png differ diff --git a/public/newsletters/260317/assets/filets_de_poulet.png b/public/newsletters/260317/assets/filets_de_poulet.png new file mode 100644 index 00000000..46fefc39 Binary files /dev/null and b/public/newsletters/260317/assets/filets_de_poulet.png differ diff --git a/public/newsletters/260317/assets/footer.png b/public/newsletters/260317/assets/footer.png new file mode 100644 index 00000000..e3d4b2f1 Binary files /dev/null and b/public/newsletters/260317/assets/footer.png differ diff --git a/public/newsletters/260317/assets/header.png b/public/newsletters/260317/assets/header.png new file mode 100644 index 00000000..faec6728 Binary files /dev/null and b/public/newsletters/260317/assets/header.png differ diff --git a/public/newsletters/260317/assets/magnets.png b/public/newsletters/260317/assets/magnets.png new file mode 100644 index 00000000..fd0b2098 Binary files /dev/null and b/public/newsletters/260317/assets/magnets.png differ diff --git a/public/newsletters/260317/assets/panier.png b/public/newsletters/260317/assets/panier.png new file mode 100644 index 00000000..b94d6d38 Binary files /dev/null and b/public/newsletters/260317/assets/panier.png differ diff --git a/public/newsletters/260317/assets/parasols.png b/public/newsletters/260317/assets/parasols.png new file mode 100644 index 00000000..e068fdda Binary files /dev/null and b/public/newsletters/260317/assets/parasols.png differ diff --git a/public/newsletters/260317/index.html b/public/newsletters/260317/index.html new file mode 100644 index 00000000..fde2b708 --- /dev/null +++ b/public/newsletters/260317/index.html @@ -0,0 +1,1509 @@ + + + + + Nouvelles chaussettes de la Boutique Collector + + + + + + + + + + + + +
+ La nouvelle paire de chausettes à collectionner : elle va faire des jaloux 😎
+
+ + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+
+ COMMANDEZ VOS GOODIES PRÉFÉRÉS
+
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + + + +
+
+ PAIRES DE CHAUSSETTES
+
+
+ À la danse ou sur un terrain de football, osez les + chaussettes collectors Le Gaulois + pleines de caractère et brillez en toute occasion. +
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + + + +
+
+ 10 DÉPART'AIMANTS
+
+
+ Kits de 10 Départ'aimants magnétiques Le Gaulois + pour compléter (ou finaliser !) votre + collection.
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + + + +
+
+ PANIERS PIQUE-NIQUE ISOTHERMES
+
+
+ Partagez des moments conviviaux avec vos proches + grâce à ce panier pique-nique au + design sobre et moderne.
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + + + +
+
+ PARASOLS
+
+
+ Égayez vos journées à la plage avec ce parasol aux + accents balnéaires.
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+
+ Avec Le Gaulois, chaque moment à + table est une occasion de + se faire plaisir... et de se faire récompenser ! + Participez sur la Boutique Collector + à travers tous nos produits : + poulet, canard, dinde, + lapin, mais aussi élaborés crus, panés, charcuterie, confits, + produits rôtis, gamme végétale + ou encore spécialités brasserie 🐔.
Il y a toujours un produit Le + Gaulois pour vous régaler + et vous faire gagner des cadeaux collector !
+
+ + + + + + +
ENCORE + DE GOODIES ICI
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ COMMENT PARTICIPER ?
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+
+ J'achète un produit Le Gaulois
+
+
+ +
+ + + + + + + + + +
+ + + + + + + +
+
+ Je déclare mon achat sur mon espace personnel avec mon ticket de + caisse ou ma facture drive +
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+
+ Je collecte mes points de fidélité à transformer en cadeaux
+
+
+ +
+ + + + + + + + + +
+ + + + + + + +
+
+ Je commande mes cadeaux préférés et les reçois directement chez moi +
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
JE COLLECTIONNE
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+
+ Entre gourmands on se comprend !
+
+
+ Vos avis comptent pour nous. Après avoir testé une recette ou un + produit Le Gaulois, prenez + quelques secondes pour la noter et partager votre avis. Vos retours + nous aident à créer des + idées toujours plus gourmandes.
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+ + + + + + + +
+
+ FILETS DE POULET CRÈME DE CITRON
+
+
+
+ +
+ + + + + + +
+ + + + + + + + + +
+ + + + + + + +
+
+ CUISSES DE CANARD MOUTARDE ET THYM
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
DONNEZ VOTRE NOTE
⭐⭐⭐⭐⭐
+
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/public/newsletters/ref/picto_club_lgl.png b/public/newsletters/ref/picto_club_lgl.png index 77231110..778b7c01 100644 Binary files a/public/newsletters/ref/picto_club_lgl.png and b/public/newsletters/ref/picto_club_lgl.png differ diff --git a/public/newsletters/ref/picto_fb.png b/public/newsletters/ref/picto_fb.png index 22d12322..931d8be5 100644 Binary files a/public/newsletters/ref/picto_fb.png and b/public/newsletters/ref/picto_fb.png differ diff --git a/public/newsletters/ref/picto_in.png b/public/newsletters/ref/picto_in.png index 2c812175..cb644fb4 100644 Binary files a/public/newsletters/ref/picto_in.png and b/public/newsletters/ref/picto_in.png differ diff --git a/public/newsletters/ref/picto_insta.png b/public/newsletters/ref/picto_insta.png index 8eac887f..a7274924 100644 Binary files a/public/newsletters/ref/picto_insta.png and b/public/newsletters/ref/picto_insta.png differ diff --git a/public/newsletters/ref/picto_yt.png b/public/newsletters/ref/picto_yt.png index c7f886c1..d97ab265 100644 Binary files a/public/newsletters/ref/picto_yt.png and b/public/newsletters/ref/picto_yt.png differ diff --git a/public/newsletters/ref/tiktok.png b/public/newsletters/ref/tiktok.png deleted file mode 100644 index a8638339..00000000 Binary files a/public/newsletters/ref/tiktok.png and /dev/null differ diff --git a/resources/assets/images/footer/socials/tiktok.png b/resources/assets/images/footer/socials/tiktok.png new file mode 100644 index 00000000..58af366f Binary files /dev/null and b/resources/assets/images/footer/socials/tiktok.png differ diff --git a/resources/ts/club/components/club/shop/RecapComponent.vue b/resources/ts/club/components/club/shop/RecapComponent.vue index f291facf..8acaeb8c 100644 --- a/resources/ts/club/components/club/shop/RecapComponent.vue +++ b/resources/ts/club/components/club/shop/RecapComponent.vue @@ -12,63 +12,35 @@
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
- -
-
-
+
+
+
+ +
+
+ +
+
+
+
+
-
-
-
- -
-
- -
-
-