ldc-legaulois/app/Http/Controllers/Club/ShopController.php
2025-06-11 09:59:07 +02:00

464 lines
19 KiB
PHP

<?php
namespace App\Http\Controllers\Club;
use App\Enums\Customers\Civility;
use App\Enums\Mercanet\PaymentResponse;
use App\Enums\Mercanet\RedirectionStatusCode;
use App\Enums\Mercanet\TransactionResponse;
use App\Enums\Shop\OrderStatus;
use App\Enums\Shop\OrderType;
use App\Exceptions\CBPayementError;
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\Models\DeliveryAddress;
use App\Models\Notification;
use App\Models\Order;
use App\Models\SellItem;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ShopController extends Controller
{
/**
* Shop index.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): \Illuminate\Contracts\View\View
{
$sellItemModels = SellItem::all();
return view('club.pages.shop.index', compact('sellItemModels'));
}
/**
* Ajax service to validate transaction details.
*
* @param \App\Http\Requests\Club\Shop\ShopBuyRequest $request
* @return \Illuminate\Http\JsonResponse
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
public function validateBuy(ShopBuyRequest $request): \Illuminate\Http\JsonResponse
{
// phpcs:enable
/** @var \App\Models\Customer */
$customerModel = auth('club')->user();
$items = [];
$orderTotalPrice = 0;
foreach ($request->cart as $item) {
$sellItem = SellItem::query()->where('id', $item['item']['id'])->firstOrFail();
$items[] = [
'id' => $sellItem->getKey(),
'name' => $sellItem->name,
'quantity' => $item['count'],
'unitPrice' => $sellItem->price,
'totalPrice' => $item['count'] * $sellItem->price,
'subject' => $sellItem->arcadia_id
];
$orderTotalPrice += $item['count'] * $sellItem->price;
}
// * Pseudo mutex let database fail (unique ref) to check if order has already been processed
try {
$order = new Order();
$order->ref = str_replace('-', '', $request->ref);
// @phpstan-ignore assign.propertyType
$order->civility = Civility::make(intval($request->coords->civility));
$order->payementType = OrderType::make(intval($request->payementMethod));
$order->status = OrderStatus::waiting;
$order->customer_id = $customerModel->getKey();
$order->fill($request->coords);
$order->saveOrFail();
} catch (QueryException $e) {
$order = Order::query()->where('ref', str_replace('-', '', $request->ref))->firstOrFail();
if ($order->payementType->equals(OrderType::check)) {
return response()->json(['redirect' => route(
'club.shop.validated',
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')
// )]);
// }
abort(500, sprintf('Order already exists but type is unkown %d', $order->getKey()));
} //end try
$order->customer()->associate($customerModel);
foreach ($items as $item) {
$order->sellItems()->withTimestamps()->attach($item['id'], ['quantity' => $item['quantity']]);
}
$order->saveOrFail();
if ($request->shipping) {
$shipping = new DeliveryAddress();
$shipping->fill($request->shipping);
$shipping->order()->associate($order);
$shipping->saveOrFail();
}
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::check->value():
try {
\session()->put(sprintf('shop.order.%d.time', $order->getKey()), \time());
\session()->save();
SendOrderToArcadia::dispatch($order);
SendShopMail::dispatch($order);
$order->status = OrderStatus::waitingCheck;
$order->saveOrFail();
} catch (Exception $e) {
report($e);
Log::critical(sprintf(
'Could not end customer transaction (save and send mail) in shop for order %d, %s',
$order->getKey(),
$e->getMessage()
));
} finally {
return response()->json(['redirect' => route('club.shop.validated', compact('order'))]);
}
default:
abort(422);
} //end switch
}
/**
* Redirection After Ajax validate.
*
* @param \App\Models\Order $order
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View
*/
public function validatedBuy(Order $order): \Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View
{
Notification::getNotifications();
$time = session()->get(sprintf('shop.order.%d.time', $order->getKey()), 0);
if (!($order->status->equals(OrderStatus::payed) || $order->status->equals(OrderStatus::waitingCheck))) {
return response()->redirectToRoute('club.shop.index');
}
// 4 minutes
if (($time + (60 * 4)) < \time()) {
return response()->redirectToRoute('club.shop.index');
}
return view('club.pages.shop.validated');
}
/**
* Pay with CB.
*
* @param \App\Models\Order $order
* @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @throws \App\Exceptions\CBPayementError Error response from Mercanet Lib.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
public function payWithCb(Order $order): \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
{
// phpcs:enable
Notification::getNotifications();
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'));
}
// 4 minutes
if (($time + (60 * 4)) < \time()) {
return view('club.pages.shop.error')
->with('error', trans('Délai d\'attente expiré, veuillez réessayer'));
}
$totalToPay = $order->sellItems->map(function ($sellItem) {
/**
* Defined in Order model
*
* @phpstan-ignore property.notFound
*/
return $sellItem->pivot->quantity * $sellItem->price;
})->sum();
// * Convert total in cents
$totalToPay = (int) (\number_format((float) $totalToPay, 2)) * 100;
$mercanet = $this->getMercanet($order);
$payment = $mercanet->QuickPayment($totalToPay, $order->ref);
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(
'Could not end customer transaction in boutique for order %d, %s',
$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'));
} //end try
}
/**
* Pas de validate ou custom request sur la réponse de mercanet,
* c'est la Lib mercanet qui fait.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Order $order
* @return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
* @throws \App\Exceptions\CBPayementError Data were altered from response.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
public function confirmCbFromMercanet(
Request $request,
Order $order
): \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory {
// phpcs:enable
try {
$data = $this->getMercanetData($request);
/** @var \App\Models\Order */
$order = Order::query()->where('ref', $data['ref'])->firstOrFail();
$mercanet = $this->getMercanet($order);
$mercanet->setResponse($request->post());
if (!$mercanet->isValid()) {
$order->status = OrderStatus::failed;
$order->save();
// @phpstan-ignore finally.exitPoint
throw new CBPayementError('Data were altered from response');
}
switch ($data['responseCode']) {
case TransactionResponse::accepted:
$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:
if (
!$order->confirmMailSent and
!$order->status->equals(OrderStatus::payed)
) {
$order->status = OrderStatus::failed;
$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 (
!$order->confirmMailSent and
!$order->status->equals(OrderStatus::payed)
) {
$order->status = OrderStatus::failed;
$order->save();
}
// @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']),
));
} //end switch
} catch (ModelNotFoundException | CBPayementError | \Exception $e) {
report($e);
Log::critical(sprintf(
'Could not end customer transaction in boutique for order %d, %s',
$order->getKey(),
$e->getMessage()
));
} finally {
// @phpstan-ignore finally.exitPoint
return response('', 200);
} //end try
}
/**
*
* Pas de validate ou custom request sur la réponse de mercanet,
* c'est la Lib mercanet qui fait.
*
* @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<string,string> 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];
}
$ref = $formDatas['transactionReference'];
$responseCode = $formDatas['responseCode'];
return compact('ref', 'responseCode', 'returnDatas');
}
}