Payment system PART2 : Stripe OK SYNC 46

- changed order pk column to id
- refactored and fixed inShop
- using php Money for transaction
This commit is contained in:
Jérémy Munsch 2023-03-08 18:01:09 +01:00
parent 3906c96a6e
commit b757d7df9d
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
34 changed files with 869 additions and 116 deletions

View file

@ -6,8 +6,8 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
/**
* @method static self reviewBasket()
* @method static self shippment()
* @method static self shippingMethod()
* @method static self shippment()
* @method static self paymentReview()
*/
final class OrderProcess extends BaseEnumRoutable

View file

@ -15,7 +15,8 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
* @method static self systemPay()
* Payment using SystemPay services
*
* @property-read string $adapter The adpater (\App\Lib\Payments\PaymentAdapter) to use for the payment mode.
* @property-read string $adapter The adpater (\App\Lib\Payments\PaymentAdapter) to use for the payment mode.
* @property-read integer $minAmount The minimal amount to use the payment mode in cents.
*/
final class PaymentModeEnum extends BaseEnumRoutable
{
@ -61,6 +62,24 @@ final class PaymentModeEnum extends BaseEnumRoutable
];
}
/**
* Enum minimal amounts in cents
*
* TODO: Set min amounts (and max) in settings.
*
* @return array<\App\Lib\Payments\PaymentAdapter>
*/
protected static function minAmount(): array
{
return [
'inShop' => 0,
// * 50 cents
'stripe' => 50,
// * 1 euro
'systemPay' => 100,
];
}
/**
* @param string $attribute
* @return integer|string
@ -70,6 +89,9 @@ final class PaymentModeEnum extends BaseEnumRoutable
if ($attribute === 'adapter') {
return $this->adapterClasses()[$this->getDefinition()];
}
if ($attribute === 'minAmount') {
return $this->minAmount()[$this->getDefinition()];
}
return parent::__get($attribute);
}
}

View file

@ -38,7 +38,7 @@ final class TransactionStatus extends BaseEnumRoutable
return [
'pending' => __('En attente'),
'failed' => __('Échec'),
'payed' => __('Validée')
'validated' => __('Validée')
];
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Enums\Stripe;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self paid()
* @method static self unpaid()
* @method static self no_payment_required()
*/
final class CheckoutPaymentStatus extends BaseEnum
{
}

View file

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Bo;
use App\Enums\Order\DeliveryStatus;
use App\Enums\Order\PaymentStatus;
use App\Enums\Order\ValidationStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Orders\ChangeOrderDeliveryStatusRequest;
@ -51,7 +50,7 @@ class OrderController extends Controller
});
}
$orders->orderBy('updated_at', 'DESC');
$orders->orderBy('created_at', 'DESC');
$orders = $orders->paginate(10);
return view('back::pages.orders.index', compact('orders', 'searchFields'));
@ -122,8 +121,6 @@ class OrderController extends Controller
$order->delivery_status = $request->delivery_status;
switch ($request->delivery_status) {
case DeliveryStatus::recovered()->value:
$order->payment_status = PaymentStatus::payed();
// Fall trough.
case DeliveryStatus::preparation()->value:
case DeliveryStatus::ready()->value:
$order->saveOrFail();

View file

@ -217,7 +217,7 @@ class CustomerController extends Controller
/** @var \App\Models\Customer\Customer */
$customer = auth('frontend')->user();
$orders = $customer->orders;
$orders = $customer->orders()->orderBy('created_at', 'DESC')->paginate();
return view('theme::pages.customer.my-orders', compact('orders'));
}

View file

@ -6,7 +6,9 @@ use App\Enums\Order\DeliveryModeEnum;
use App\Enums\GeneralConfigType;
use App\Enums\Order\DeliveryStatus;
use App\Enums\Order\OrderProcess;
use App\Enums\Order\PaymentModeEnum;
use App\Enums\Order\PaymentStatus;
use App\Enums\Order\TransactionStatus;
use App\Enums\Order\TransactionType;
use App\Enums\Order\ValidationStatus;
use App\Http\Controllers\Controller;
@ -17,6 +19,7 @@ use App\Http\Requests\Fo\Orders\ShippingModeFormRequest;
use App\Http\Resources\ShopResource;
use App\Mail\NewOrderIncome;
use App\Models\Customer\Customer;
use App\Models\Merchandise\Product;
use App\Models\Payment\Transaction;
use App\Models\Shop\DeliveryMode;
use App\Models\Site\GeneralConfig;
@ -25,6 +28,7 @@ use App\Models\Shop\Shop;
use App\Models\Shop\Timeslot;
use App\Notifications\Order\OrderCreated;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
@ -63,6 +67,7 @@ class OrderController extends Controller
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->setOrderProcessStep(OrderProcess::reviewBasket());
/** @var \Gloudemans\Shoppingcart\Cart */
@ -94,6 +99,7 @@ class OrderController extends Controller
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->setOrderProcessStep(OrderProcess::shippingMethod(), OrderProcess::reviewBasket());
// * CREATE ORDER
@ -138,6 +144,7 @@ class OrderController extends Controller
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->assertDoesHaveCurrentOrder();
$this->setOrderProcessStep(OrderProcess::shippingMethod(), OrderProcess::reviewBasket());
@ -175,6 +182,7 @@ class OrderController extends Controller
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->assertDoesHaveCurrentOrder();
$this->assertDeliveryModeIsValid();
$this->setOrderProcessStep(OrderProcess::shippment(), OrderProcess::shippingMethod());
@ -223,6 +231,7 @@ class OrderController extends Controller
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->assertDoesHaveCurrentOrder();
$this->assertDeliveryModeIsValid();
$this->setOrderProcessStep(OrderProcess::shippment(), OrderProcess::shippingMethod());
@ -238,14 +247,16 @@ class OrderController extends Controller
DB::transaction(function () use ($shop, $creneau) {
/** @var \App\Models\Shop\Order $order */
$order = Order::getCurrentOrder();
// * Assign order uuid we would use, if none already.
if (!$order->uuid) {
$order->uuid = Str::uuid();
// * Assign order id we would use, if none already.
if (!$order->id) {
$order->id = Str::uuid();
}
Session::put('currentOrder', $order);
/** @var \App\Models\Shop\Timeslot */
$timeSlot = Timeslot::query()->where('order_uuid', $order->uuid)->firstOrNew();
$timeSlot = Timeslot::query()->whereHas('order', function (Builder $q) use ($order) {
$q->where('id', $order->id);
})->firstOrNew();
$timeSlot->order()->associate($order);
$timeSlot->shop()->associate($shop);
$timeSlot->slot = $creneau;
@ -268,6 +279,7 @@ class OrderController extends Controller
$this->assertDoesNotHaveEmptyBasket();
$this->assertDoesHaveCurrentOrder();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->assertDeliveryModeIsValid();
$this->setOrderProcessStep(OrderProcess::shippment(), OrderProcess::shippingMethod());
@ -307,12 +319,15 @@ class OrderController extends Controller
/**
* Valider le panier avant achat.
*
* * --- 4 ---
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function review()
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertBasketDidNotChange();
$this->assertBasketHasMinAmount();
$this->assertDoesHaveCurrentOrder();
$this->assertDeliveryModeIsValid();
$this->setOrderProcessStep(OrderProcess::paymentReview(), OrderProcess::shippment());
@ -342,6 +357,8 @@ class OrderController extends Controller
/**
* Setup Order for submit
*
* * --- 4 ---
*
* @param \App\Http\Requests\Fo\Orders\CheckoutRequest $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @throws \RuntimeException If payment method is unhandled somehow.
@ -373,43 +390,81 @@ class OrderController extends Controller
// * Check Payment mode
Gate::authorize('isAvailable', [PaymentMode::class, $paymentMode]);
$transaction = Transaction::getNewInstance(
$order,
// * Get Transaction
$transaction = Transaction::getCurrentTransaction() ??
Transaction::getNewInstance(
$paymentMode,
TransactionType::debit(),
$order->total_ttc,
);
$transaction->refresh();
// * If transaction is already payed.
if ($transaction->status->equals(TransactionStatus::validated())) {
// ! Prevent double checkout (double transaction), just let IPN (webhooks) handle this.
$e = new \RuntimeException('La transaction a déjà été traitée');
report($e);
$this->detroyCurrentBasket();
abort(500, trans($e->getMessage()));
}
// * If previously failed get a new one.
if ($transaction->status->equals(TransactionStatus::failed())) {
$transaction = Transaction::getNewInstance(
$paymentMode,
TransactionType::debit(),
$order->total_ttc,
);
}
// * If customer switched transaction mode.
if (!$transaction->payment_mode->equals($paymentMode)) {
// * Reuse previous
$transaction = Transaction::query()->whereHas('order', function (Builder $q) use ($order) {
$q->where('id', $order->id);
})->where('payment_mode', $paymentMode)->where('status', TransactionStatus::pending())
->first() ?? Transaction::getNewInstance(
$paymentMode,
TransactionType::debit(),
$order->total_ttc,
);
// * Remove other pending in shop transactions
Transaction::query()->whereHas('order', function (Builder $q) use ($order) {
$q->where('id', $order->id);
})->where('status', TransactionStatus::pending())->where('id', '!=', $transaction->id)
->where('payment_mode', PaymentModeEnum::inShop())->delete();
}
// * If payment mode is boutique just use the previous transaction if there was one.
if ($transaction->payment_mode->equals(PaymentModeEnum::inShop())) {
$transaction = Transaction::query()->whereHas('order', function (Builder $q) use ($order) {
$q->where('id', $order->id);
})->where('payment_mode', PaymentModeEnum::inShop())
->where('status', TransactionStatus::pending())->first() ?? $transaction;
}
// * Checkout using selected Payment system.
return $transaction->getAdapter()->checkout(
$customer,
$order,
$transaction,
// * When succeeded
function (Customer $customer, Order $order) {
// * Notification has been sent
$order->mail_sent = true;
function (Customer $customer, Order $order, Transaction $transaction) {
// * Associate transaction to order
$transaction->order()->associate($order);
// * Enregistrement commande + envoi de mail.
if (!$order->save()) {
// * Enregistrement commande avant checkout.
if (!$order->save() or !$transaction->save()) {
report(new \RuntimeException("Order saving error happened."));
throw ValidationException::withMessages([
'error' => trans('Une erreur est survenue, veuillez réssayer')
]);
}
// * Notifiy customer
$customer->notify(new OrderCreated($order));
/** @var object|null */
$notificationMail = \optional(\optional(GeneralConfig::query()
->where('type', GeneralConfigType::global())
->first())->data)->confirmed_orders_destination_email;
if (!\optional($notificationMail)->value) {
throw new \RuntimeException('Could not get new order email notification setting');
}
Mail::to($notificationMail->value)->send(new NewOrderIncome($order));
self::detroyCurrentBasket();
// * Save Transaction
Session::put('currentTransaction', $transaction->refresh());
// * Save Order
Session::put('currentOrder', $order->refresh());
},
// ! If failed.
function () {
@ -420,6 +475,131 @@ class OrderController extends Controller
);
}
/**
* Redirection après paiement et validation
*
* * --- 5 ---
*
* ! Middleware Url signée ici
*
* * La méthode sers a récupérer la session après redirection du module de paiment.
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function checkout()
{
/** @var \App\Models\Customer\Customer|null $customer */
$customer = auth('frontend')->user();
/** @var \App\Models\Shop\Order|null $order */
$order = Order::getCurrentOrder();
/** @var \App\Models\Payment\Transaction|null $transaction */
$transaction = Transaction::getCurrentTransaction();
if (!$customer or !$order or !$transaction) {
\redirect()->route('order-tunnel.review')
->with('warning', trans('La session est requise'))
->throwResponse();
}
// * Force clone here to prevent session save model state (If something goes wrong).
$order = clone $order;
$transaction = clone $transaction;
// * If transaction has already been processed.
if (!$transaction->status->equals(TransactionStatus::pending())) {
\redirect()->route('order-tunnel.review')
->with('warning', trans('Étape précédente requise'))
->throwResponse();
}
if (!($transaction = $transaction->getAdapter()->checkoutIsResponseIsValid($customer, $transaction))) {
\abort(400, trans('Impossible de valider la transaction'));
}
// * Save Transaction
Session::put('currentTransaction', $transaction);
// * Save Order
Session::put('currentOrder', $order);
return view('back::pages.order-tunnel-checkout');
}
/**
* Redirection après paiement et validation
*
* * --- 5 ---
*
* * La méthode sers a récupérer la session après redirection du module de paiment.
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function checkoutVerify()
{
$this->assertDoesNotHaveEmptyBasket();
$this->assertDoesHaveCurrentOrder();
$this->assertDoesHaveCurrentTransaction();
if (!$this->checkThatActualProcessStepIs(OrderProcess::paymentReview())) {
\redirect()->route('order-tunnel.review')
->with('warning', trans('Étape précédente requise'))
->throwResponse();
}
return DB::transaction(function () {
/** @var \App\Models\Customer\Customer $customer */
$customer = auth('frontend')->user();
// * Force clone here to prevent session save model state (If something goes wrong).
/** @var \App\Models\Shop\Order $order */
$order = clone Order::getCurrentOrder();
// * Force clone here to prevent session save model state (If something goes wrong).
/** @var \App\Models\Payment\Transaction $transaction */
$transaction = clone Transaction::getCurrentTransaction();
// * Order completed email already sent.
if ($order->exists and $transaction->exists and $order->mail_sent) {
self::detroyCurrentBasket();
return \view('theme::pages.order-tunnel.done', \compact('order'));
}
$transaction->getAdapter()->checkoutVerify($transaction);
$transaction->saveOrFail();
// * Notification has been sent
Order::query()->where('id', $order->id)->update([
'mail_sent' => true
]);
$order->refresh();
// * Notifiy customer
$customer->notify(new OrderCreated($order));
/** @var object|null */
$notificationMail = \optional(\optional(GeneralConfig::query()
->where('type', GeneralConfigType::global())
->first())->data)->confirmed_orders_destination_email;
if (!\optional($notificationMail)->value) {
throw new \RuntimeException('Could not get new order email notification setting');
}
if ($order->validation_status->equals(ValidationStatus::pending())) {
// * Send mail after reponse to BO user.
dispatch(function () use (
$notificationMail,
$order
) {
Mail::to($notificationMail->value)->send(new NewOrderIncome($order));
})->afterResponse();
}
self::detroyCurrentBasket();
return view('theme::pages.order-tunnel.done', \compact('order'));
});
}
// * --- FIN TUNNEL DE COMMANDE ---
// * PRIVATE METHODS
@ -443,7 +623,6 @@ class OrderController extends Controller
*/
private function assertDoesHaveCurrentOrder()
{
$order = Order::getCurrentOrder();
if (
$order = Order::getCurrentOrder() and
$order->payment_status and
@ -476,6 +655,7 @@ class OrderController extends Controller
}
Session::remove('currentOrder');
Session::remove('currentTransaction');
Session::remove('PreviousOrderStep');
Session::remove('OrderStep');
Session::remove('deliveryModePickSlot');
@ -523,6 +703,49 @@ class OrderController extends Controller
}
}
/**
* Assert basket has the minimal amount required for payment
*
* @return void
*/
private function assertBasketHasMinAmount(): void
{
/** @var \Gloudemans\Shoppingcart\Cart */
$basket = app('basket');
$sampleOrder = new Order();
$sampleOrder->assignBasket($basket);
$minAmountForTransaction = \collect(PaymentModeEnum::toArray())
->max(function (PaymentModeEnum $paymentMode) {
return $paymentMode->minAmount;
});
if ($sampleOrder->total_ttc < $minAmountForTransaction) {
\redirect()->route('home')
->with('warning', trans('Le montant minimal pour une commande est de :minAmount', [
'minAmount' => Product::fmtCurrency($minAmountForTransaction)
]))->throwResponse();
}
}
/**
* Assert user does not have an empty basket
* or redirect with warning
*
* @return void
*/
private function assertDoesHaveCurrentTransaction()
{
$transaction = Transaction::getCurrentTransaction();
if (
!$transaction or ($transaction->status and
$transaction->status->equals(TransactionStatus::failed())
)
) {
\redirect()->route('order-tunnel.review')
->with('warning', trans('La transaction a échoué'))
->throwResponse();
}
}
/**
* Assert delivery mode si still available.
*
@ -561,19 +784,51 @@ class OrderController extends Controller
$previousSteps->put($previousStep->value, $previousStep);
Session::put('PreviousOrderStep', $previousSteps);
}
if (!$previousSteps->count()) {
\redirect()->route('home')
->with('warning', trans('Vous devez d\'abord valider votre panier'))
->throwResponse();
}
if (!$previousSteps->contains($assertDid)) {
throw ValidationException::withMessages(['Étape précédente requise']);
}
$this->assertDidProcessStep($assertDid);
} //end if
Session::put('OrderStep', $step);
}
/**
* Assert the the step is done.
*
* @param \App\Enums\Order\OrderProcess $assertDid
* @return void
* @throws \Illuminate\Validation\ValidationException If previous was not done.
*/
private function assertDidProcessStep(OrderProcess $assertDid): void
{
if (!Session::get('PreviousOrderStep')) {
Session::put('PreviousOrderStep', collect());
}
/** @var \Illuminate\Support\Collection */
$previousSteps = Session::get('PreviousOrderStep');
if (!$previousSteps->count()) {
\redirect()->route('home')
->with('warning', trans('Vous devez d\'abord valider votre panier'))
->throwResponse();
}
if (!$previousSteps->contains($assertDid)) {
throw ValidationException::withMessages(['Étape précédente requise']);
}
}
/**
* Assert the actual step is.
*
* @param \App\Enums\Order\OrderProcess $assertActual
* @return boolean
* @throws \Illuminate\Validation\ValidationException If previous was not done.
*/
private function checkThatActualProcessStepIs(OrderProcess $assertActual): bool
{
/** @var \App\Enums\OrderProcess|null */
$orderStep = Session::get('OrderStep');
return $orderStep and $orderStep->equals($assertActual);
}
/**
* Fill order shipping with in shop infos
*
@ -583,8 +838,6 @@ class OrderController extends Controller
*/
private function fillShippingFromShopInfo(Order $order)
{
/** @var \App\Models\Customer\Customer */
$user = auth('frontend')->user();
if ($order->deliveryMode->mode->equals(DeliveryModeEnum::inShop())) {
/** @var \App\Models\Shop\Timeslot|null */
if (!($timeslot = $order->timeslot()->first())) {

View file

@ -4,37 +4,67 @@ namespace App\Lib\Payments\Adapters;
use App\Lib\Payments\PaymentAdapter;
use App\Models\Customer\Customer;
use App\Models\Payment\Transaction;
use App\Models\Shop\Order;
use Illuminate\Http\RedirectResponse;
class InShopPaymentAdapter extends PaymentAdapter
{
/**
* Generate the checkout reponse to initiate a transaction.
*
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Shop\Order $order
* @param \Closure(\App\Models\Customer\Customer,\App\Models\Shop\Order): void $succeed
* @param \Closure(\Exception): void $failed
* @return \Illuminate\Http\Response|Illuminate\View\View
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
* @phpcs:disable Generic.Files.LineLength.TooLong
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Shop\Order $order
* @param \App\Models\Payment\Transaction $transaction
* @param \Closure(\App\Models\Customer\Customer,\App\Models\Shop\Order,\App\Models\Payment\Transaction): void $succeed
* @param \Closure(\Exception): void $failed
* @return \Illuminate\Http\RedirectResponse
*/
public function checkout(
Customer $customer,
Order $order,
Transaction $transaction,
\Closure $succeed,
\Closure $failed
): object {
return $this->wrapAction(function () use ($customer, $order, $succeed) {
// * Enregistrement commande + envoi de mail.
): RedirectResponse {
// phpcs:enable
return $this->wrapAction(function () use ($customer, $order, $transaction, $succeed) {
// * Enregistrement commande.
if (!$order->save()) {
throw new \RuntimeException("Order saving error happened.");
}
if ($succeed) {
$succeed($customer, $order);
$succeed($customer, $order, $transaction);
}
return \view('theme::pages.order-tunnel.done', \compact('order'));
return \response()->redirectTo($this->successRoute($order));
}, $failed);
}
// phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamTag
/**
* @inheritDoc
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.MissingReturn
*/
public function checkoutIsResponseIsValid(Customer $customer, Transaction $transaction): ?Transaction
{
// phpcs:enable
return request()->hasValidSignature() ? $transaction : null;
}
/**
* Action after the checkout redirection (on the next page)
*
* @param \App\Models\Payment\Transaction $transaction
* @return void
* @throws \Throwable If something goes wrong.
*/
public function checkoutVerify(Transaction $transaction): void
{
}
}

View file

@ -2,9 +2,134 @@
namespace App\Lib\Payments\Adapters;
use App\Enums\Order\TransactionStatus;
use App\Enums\Order\ValidationStatus;
use App\Enums\Stripe\CheckoutPaymentStatus;
use App\Lib\Payments\PaymentAdapter;
use App\Models\Customer\Customer;
use App\Models\Payment\Transaction;
use App\Models\Shop\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
class StripePaymentAdapter extends PaymentAdapter
{
// Emtpy class.
/**
* Get success route
*
* ! Overload signed route for this payment method: Strip does it already.
*
* @param \App\Models\Shop\Order $order
* @param array $parameters
* @return string
*/
protected function successRoute(Order $order, array $parameters = []): string
{
$parameters['order'] = $order;
// * Fix urlencoded '{CHECKOUT_SESSION_ID}' so Stripe can interpolate it.
return Str::replace(
\urlencode('{CHECKOUT_SESSION_ID}'),
'{CHECKOUT_SESSION_ID}',
route('order-tunnel.checkout', $parameters)
);
}
/**
* Generate the checkout reponse to initiate a transaction.
*
* * This is a unique transaction with qty 1 and the desired amount for the order.
*
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
* @phpcs:disable Generic.Files.LineLength.TooLong
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Shop\Order $order
* @param \App\Models\Payment\Transaction $transaction
* @param \Closure(\App\Models\Customer\Customer,\App\Models\Shop\Order,\App\Models\Payment\Transaction): void $succeed
* @param \Closure(\Exception): void $failed
* @return \Illuminate\Http\RedirectResponse
*/
public function checkout(
Customer $customer,
Order $order,
Transaction $transaction,
\Closure $succeed,
\Closure $failed
): RedirectResponse {
// phpcs:enable
$succeed($customer, $order, $transaction);
return $this->wrapAction(function () use ($customer, $order, $transaction) {
return $customer->checkout(
[[
'price_data' => [
'currency' => Str::lower($transaction->currency->getCode()),
'unit_amount' => $order->total_ttc,
'product_data' => ['name' => trans('Commande :ref', ['ref' => $order->ref])]
],
'quantity' => 1,
]],
[
'success_url' => $this->successRoute($order, ['session_id' => '{CHECKOUT_SESSION_ID}']),
'cancel_url' => $this->cancelRoute($order),
]
)->toResponse(\request());
}, $failed);
}
// phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamTag
/**
* @inheritDoc
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.MissingReturn
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
public function checkoutIsResponseIsValid(Customer $customer, Transaction $transaction): ?Transaction
{
// phpcs:enable
try {
// * Get stripe status, wont check status, since it can only be validated,
// * only call should succeed.
/** @throws \Stripe\Exception\ApiErrorException If the request fails */
/** @var \Stripe\Checkout\Session */
$stripeCheckout = $customer->stripe()->checkout->sessions->retrieve(request()->get('session_id'));
$checkoutStatus = CheckoutPaymentStatus::make($stripeCheckout->payment_status);
switch ($checkoutStatus) {
case CheckoutPaymentStatus::no_payment_required():
throw new \RuntimeException('Stripe checkout no_payment_required is not allowed');
case CheckoutPaymentStatus::paid():
$transaction->status = TransactionStatus::validated();
break;
case CheckoutPaymentStatus::unpaid():
$transaction->status = TransactionStatus::failed();
break;
default:
throw new \RuntimeException("Stripe checkout status '{$checkoutStatus->label}' is un handled");
}
return $transaction;
} catch (\Stripe\Exception\ApiErrorException $e) {
return null;
} //end try
}
/**
* Action after the checkout redirection (on the next page)
*
* @param \App\Models\Payment\Transaction $transaction
* @return void
* @throws \Throwable If something goes wrong.
* @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
public function checkoutVerify(Transaction $transaction): void
{
// phpcs:enable
/** @var \App\Models\Shop\Order|null */
if (!($order = $transaction->order)) {
throw new \RuntimeException('Impossible d\'obtenir la commande de la transaction');
}
// * La commande est automatiquement validée lors d'un paiement en ligne.
$order->validation_status = ValidationStatus::validated();
$order->save();
}
}

View file

@ -3,8 +3,22 @@
namespace App\Lib\Payments\Adapters;
use App\Lib\Payments\PaymentAdapter;
use App\Models\Customer\Customer;
use App\Models\Payment\Transaction;
class SystemPayPaymentAdapter extends PaymentAdapter
{
// Emtpy class.
// phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamTag
/**
* @inheritDoc
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.MissingReturn
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
public function checkoutIsResponseIsValid(Customer $customer, Transaction $transaction): ?Transaction
{
// phpcs:enable
return request()->hasValidSignature() ? $transaction : null;
}
}

View file

@ -6,7 +6,9 @@ use App\Lib\Helpers\ToolboxHelper;
use App\Models\Customer\Customer;
use App\Models\Payment\Transaction;
use App\Models\Shop\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\URL;
abstract class PaymentAdapter
{
@ -27,10 +29,10 @@ abstract class PaymentAdapter
*
* @param \Closure $action
* @param \Closure(\Exception): void $failure
* @return \Illuminate\Http\Response|Illuminate\View\View
* @return \Illuminate\Http\RedirectResponse
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
protected function wrapAction(\Closure $action, \Closure $failure): object
protected function wrapAction(\Closure $action, \Closure $failure): RedirectResponse
{
// phpcs:enable
try {
@ -54,7 +56,7 @@ abstract class PaymentAdapter
protected function successRoute(Order $order, array $parameters = []): string
{
$parameters['order'] = $order;
return route('orders.processed', $parameters);
return URL::signedRoute('order-tunnel.checkout', $parameters, \now()->addMinutes(3));
}
/**
@ -67,24 +69,48 @@ abstract class PaymentAdapter
protected function cancelRoute(Order $order, array $parameters = []): string
{
$parameters['order'] = $order;
return route('orders.review', $order);
return route('order-tunnel.review', $order);
}
/**
* Generate the checkout reponse to initiate a transaction.
*
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Shop\Order $order
* @param \Closure(\App\Models\Customer\Customer,\App\Models\Shop\Order): void $succeed
* @param \Closure(\Exception): void $failed
* @return \Illuminate\Http\Response|Illuminate\View\View
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
* @phpcs:disable Generic.Files.LineLength.TooLong
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Shop\Order $order
* @param \App\Models\Payment\Transaction $transaction
* @param \Closure(\App\Models\Customer\Customer,\App\Models\Shop\Order,\App\Models\Payment\Transaction): void $succeed
* @param \Closure(\Exception): void $failed
* @return \Illuminate\Http\Response|Illuminate\View\View
*/
abstract public function checkout(
Customer $customer,
Order $order,
Transaction $transaction,
\Closure $succeed,
\Closure $failed
): object;
): RedirectResponse;
// phpcs:enable
/**
* Check if return from payment method is valid.
*
* ! No save operation must be done here !
*
* @param \App\Models\Customer\Customer $customer
* @param \App\Models\Payment\Transaction $transaction
* @return \App\Models\Payment\Transaction|null
*/
abstract public function checkoutIsResponseIsValid(Customer $customer, Transaction $transaction): ?Transaction;
/**
* Action after the checkout redirection (on the next page)
*
* @param \App\Models\Payment\Transaction $transaction
* @return void
* @throws \Throwable If something goes wrong.
*/
abstract public function checkoutVerify(Transaction $transaction): void;
}

View file

@ -59,6 +59,15 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
});
static::updating(function (self $model) {
static::assertEmailIsUnique($model->id, $model->email);
// * Sync with stripe if already known by stripe.
if ($model->stripe_id) {
$model->syncStripeCustomerDetails();
}
});
static::deleting(function () {
// Comme sur LG.
// FIXME: RGPD friendly solution must be applied here to delete old customers.
throw new \RuntimeException('This is not allowed');
});
}

View file

@ -3,6 +3,7 @@
namespace App\Models\Payment;
use App\Enums\Order\PaymentModeEnum;
use App\Enums\Order\PaymentStatus;
use App\Enums\Order\TransactionStatus;
use App\Enums\Order\TransactionType;
use App\Lib\Helpers\ToolboxHelper;
@ -10,6 +11,7 @@ use App\Lib\Payments\PaymentAdapter;
use App\Models\Shop\Order;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Kwaadpepper\Enum\Traits\CastsEnums;
use Money\Currency;
@ -65,11 +67,13 @@ class Transaction extends Model
static::creating(function (self $model) {
// * Référence unique par jour.
$model->reference = ToolboxHelper::getUniqueReference(self::class);
$model->mutateOrderStatus();
});
static::updating(function (self $model) {
if (!$model->reference) {
throw new \RuntimeException('Missing reference');
}
$model->mutateOrderStatus();
});
static::deleting(function () {
throw new \RuntimeException('Removing a transaction is not allowed');
@ -81,7 +85,6 @@ class Transaction extends Model
/**
* Create a new instance of the given model.
*
* @param \App\Models\Shop\Order $order
* @param \App\Enums\Order\PaymentModeEnum $mode
* @param \App\Enums\Order\TransactionType $type
* @param integer $amount
@ -90,7 +93,6 @@ class Transaction extends Model
* @throws \RuntimeException If amount is below 0.
*/
public static function getNewInstance(
Order $order,
PaymentModeEnum $mode,
TransactionType $type,
int $amount = 0,
@ -106,7 +108,6 @@ class Transaction extends Model
$transaction->status = TransactionStatus::pending();
$transaction->type = $type;
$transaction->payment_mode = $mode;
$transaction->order()->associate($order);
return $transaction;
}
@ -127,6 +128,18 @@ class Transaction extends Model
return app($this->payment_mode->adapter);
}
/**
* Get updated current session order
*
* @return null|\App\Models\Payment\Transaction
*/
public static function getCurrentTransaction(): ?self
{
/** @var \App\Models\Payment\Transaction|null $transaction */
$transaction = Session::get('currentTransaction');
return $transaction;
}
/**
* Fetch the associated order.
*
@ -137,6 +150,39 @@ class Transaction extends Model
return $this->order;
}
/**
* Set related Order payment status using this transaction actual status.
*
* @return void
* @throws \RuntimeException If this transaction is not associated with and order.
*/
public function mutateOrderStatus(): void
{
/** @var \App\Models\Shop\Order $relatedOrder */
if (!($relatedOrder = $this->order)) {
throw new \RuntimeException("Any transaction has to be associated with an order, id '{$this->id}'");
}
// * Ignore pending here as default status
switch ($this->status) {
case TransactionStatus::validated():
// * Make distinction between payment and refund (Full/Partial)
$paymentStatus = PaymentStatus::payed();
if ($this->type->equals(TransactionType::credit())) {
$paymentStatus = $relatedOrder->isFullyRefunded() ?
PaymentStatus::refunded() : PaymentStatus::refundedPartial();
}
$relatedOrder->payment_status = $paymentStatus;
break;
case TransactionStatus::failed():
// * Just set failure on any failed transaction.
$relatedOrder->payment_status = PaymentStatus::failed();
break;
}
$relatedOrder->save();
}
// * GETTER
/**
* Get the transation amount
*
@ -147,6 +193,18 @@ class Transaction extends Model
return new Money($this->attributes['amount'], $this->currency);
}
/**
* Get the transation currency
*
* @return \Money\Currency
*/
public function getCurrencyAttribute(): Currency
{
return new Currency(Str::upper($this->attributes['currency']));
}
// * SETTER
/**
* Set the transation amount
*
@ -159,16 +217,6 @@ class Transaction extends Model
$this->attributes['currency'] = \strval($value->getCurrency());
}
/**
* Get the transation currency
*
* @return \Money\Currency
*/
public function getCurrencyAttribute(): Currency
{
return new Currency(Str::upper($this->attributes['currency']));
}
/**
* Set the transation currency
*

View file

@ -4,6 +4,7 @@ namespace App\Models\Shop;
use App\Enums\Order\DeliveryStatus;
use App\Enums\Order\PaymentStatus;
use App\Enums\Order\TransactionType;
use App\Enums\Order\ValidationStatus;
use App\Lib\Helpers\ToolboxHelper;
use App\Models\Customer\Customer;
@ -15,8 +16,11 @@ use Gloudemans\Shoppingcart\Cart;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Kwaadpepper\Enum\Traits\CastsEnums;
use Money\Currency;
use Money\Money;
/**
* Order
@ -44,13 +48,6 @@ class Order extends Model
use CastsEnums;
use HasFactory;
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'uuid';
/**
* Indicates if the IDs are auto-incrementing.
*
@ -128,7 +125,7 @@ class Order extends Model
if ($model->validation_status->value !== $model->getOriginal('validation_status')) {
// * Notifiy customer
$model->customer->notify(new OrderValidationStatusChanged($model));
Session::flash('info', trans('Le client recevra une notification sur l\'état de validation.'));
Session::flash('bo.info', trans('Le client recevra une notification sur l\'état de validation.'));
}
if (
$model->delivery_status->equals(DeliveryStatus::ready()) and
@ -136,7 +133,7 @@ class Order extends Model
) {
// * Notifiy customer
$model->customer->notify(new OrderDeliveryStatusChanged($model));
Session::flash('info', trans('Le client recevra une notification sur \'état de livraison.'));
Session::flash('bo.info', trans('Le client recevra une notification sur \'état de livraison.'));
}
});
}
@ -144,13 +141,13 @@ class Order extends Model
// * RELATIONSHIPS
/**
* BelongsToMany transactions relation
* HasMany transactions relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function transactions()
{
return $this->belongsToMany(Transaction::class);
return $this->hasMany(Transaction::class);
}
/**
@ -232,6 +229,59 @@ class Order extends Model
// * METHODS
/**
* Total payed by the customer
*
* @return \Money\Money
*/
public function getTotalPayed(): Money
{
return $this->transactions->reduce(function ($previous, Transaction $actual) {
/** @var \Money\Money|null $previous */
/** @var \App\Models\Payment\Transaction $actual */
$out = new Money(0, new Currency(Str::lower(env('CASHIER_CURRENCY', 'eur'))));
if ($previous) {
$out->add($previous);
}
if ($actual->type->equals(TransactionType::debit())) {
$out->add($actual->amount);
}
return $out;
});
}
/**
* Total refunded to the customer
*
* @return \Money\Money
*/
public function getTotalRefunded(): Money
{
return $this->transactions->reduce(function ($previous, Transaction $actual) {
/** @var \Money\Money|null $previous */
/** @var \App\Models\Payment\Transaction $actual */
$out = new Money(0, new Currency(Str::lower(env('CASHIER_CURRENCY', 'eur'))));
if ($previous) {
$out->add($previous);
}
if ($actual->type->equals(TransactionType::credit())) {
$out->add($actual->amount);
}
return $out;
});
}
/**
* Has the customer been completelly refunded ?
*
* @return boolean
*/
public function isFullyRefunded(): bool
{
return $this->getTotalPayed()->subtract($this->getTotalRefunded())
->lessThanOrEqual(new Money(0, new Currency(Str::lower(env('CASHIER_CURRENCY', 'eur')))));
}
/**
* Setup reference
*
@ -262,8 +312,7 @@ class Order extends Model
!$model->can_be_cancelled and
$model->validation_status->equals(ValidationStatus::cancelled())
) {
throw ValidationException::withMessages([
'messages' => "La commande '{$model->uuid}' ne peut être annulée"
throw ValidationException::withMessages(['messages' => "La commande '{$model->id}' ne peut être annulée"
]);
}
}
@ -279,7 +328,7 @@ class Order extends Model
{
if ($model->getOriginal('validation_status') === ValidationStatus::cancelled()->value) {
throw ValidationException::withMessages([
'messages' => "La commande '{$model->uuid}' ne peut être validée après avoir été annulée"
'messages' => "La commande '{$model->id}' ne peut être validée après avoir été annulée"
]);
}
}
@ -298,13 +347,13 @@ class Order extends Model
throw ValidationException::withMessages([
'messages' =>
// phpcs:ignore Generic.Files.LineLength.TooLong
"L'état de livraison de la commande ne peut être modifié car la commande '{$model->uuid}' n'est pas validée"
"L'état de livraison de la commande ne peut être modifié car la commande '{$model->id}' n'est pas validée"
]);
}
if ($model->getOriginal('validation_status') === ValidationStatus::cancelled()->value) {
throw ValidationException::withMessages([
'messages' =>
"L'état de livraison de la commande ne peut être modifié car la commande '{$model->uuid}' est annulée"
"L'état de livraison de la commande ne peut être modifié car la commande '{$model->id}' est annulée"
]);
}
}
@ -319,8 +368,8 @@ class Order extends Model
/** @var \App\Models\Shop\Order|null $order */
if ($order = Session::get('currentOrder')) {
// * Refresh order if exists in DB.
$order = Order::where('uuid', $order->uuid)
->orWhere('reference', $order->ref)
$order = Order::query()->where('id', $order->id)
->orWhere('reference', $order->reference)
->first() ?? $order;
}
return $order;

View file

@ -36,7 +36,7 @@ class Timeslot extends Model
*/
public function order()
{
return $this->belongsTo(Order::class, 'order_uuid', 'uuid');
return $this->belongsTo(Order::class);
}
/**

View file

@ -18,6 +18,8 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\URL;
use Laravel\Cashier\Cashier;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\IntlMoneyFormatter;
class AppServiceProvider extends ServiceProvider
{
@ -62,6 +64,18 @@ class AppServiceProvider extends ServiceProvider
));
Carbon::setLocale(config('app.locale'));
// * Php Money formater
App::singleton('phpMoneyFormatter', function () {
// * Number format using app locale.
$currencies = new ISOCurrencies();
$numberFormatter = new \NumberFormatter(\sprintf(
'%s_%s',
config('app.locale'),
\strtoupper(config('app.locale'))
), \NumberFormatter::CURRENCY);
return new IntlMoneyFormatter($numberFormatter, $currencies);
});
// * Basket singleton
App::singleton('basket', function () {
return Cart::instance('basket');

View file

@ -14,6 +14,7 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"ext-intl": "*",
"axlon/laravel-postal-code-validation": "^3.3",
"bumbummen99/shoppingcart": "^4.0",
"diglactic/laravel-breadcrumbs": "^6.1",

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('customers', function (Blueprint $table) {
$table->string('stripe_id')->nullable()->index()->after('id')
->comment('Obligatoire avec cashier pour utiliser Stripe.');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn([
'stripe_id',
]);
});
}
};

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('subscription_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('subscription_id');
$table->string('stripe_id')->unique();
$table->string('stripe_product');
$table->string('stripe_price');
$table->integer('quantity')->nullable();
$table->timestamps();
$table->unique(['subscription_id', 'stripe_price']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('subscription_items');
}
};

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeOrderPkNameToStandard extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->renameColumn('uuid', 'id');
});
Schema::table('timeslots', function (Blueprint $table) {
$table->renameColumn('order_uuid', 'order_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->renameColumn('id', 'uuid');
});
Schema::table('timeslots', function (Blueprint $table) {
$table->renameColumn('order_id', 'order_uuid');
});
}
}

View file

@ -16,6 +16,8 @@ class CreateTransactionsTable extends Migration
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->string('reference')->comment('Référence de la transaction');
$table->unsignedInteger('amount')->comment('Montant de la transaction en centimes');
$table->string('currency', 4)->comment('Devise de la transation');
@ -26,8 +28,8 @@ class CreateTransactionsTable extends Migration
$table->unsignedTinyInteger('status')
->comment('Enum Transaction Status (état de la transaction)');
$table->uuid('order_uuid')->comment('Commande concernée par la transaction');
$table->foreign('order_uuid')->references('uuid')->on('orders')->cascadeOnDelete();
$table->uuid('order_id')->comment('Commande concernée par la transaction');
$table->foreign('order_id')->references('id')->on('orders')->cascadeOnDelete();
$table->timestamps();
});

@ -1 +1 @@
Subproject commit caa09adba36f08a9cd0033260430b8197835a3c9
Subproject commit 042cb97947ebd020a869556861e75f81e8b34ee9

View file

@ -1,12 +1,12 @@
@if(!request()->routeIs('login'))
@if ($message = Session::get('success'))
@if ($message = Session::get('success', Session::get('bo.success')))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Fermer') }}"></button>
</div>
@endif
@if ($message = Session::get('error'))
@if ($message = Session::get('error', Session::get('bo.error')))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Fermer') }}"></button>
@ -14,7 +14,7 @@
@endif
@if ($message = Session::get('warning'))
@if ($message = Session::get('warning', Session::get('bo.warning')))
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Fermer') }}"></button>
@ -22,7 +22,7 @@
@endif
@if ($message = Session::get('info'))
@if ($message = Session::get('info', Session::get('bo.info')))
<div class="alert alert-info alert-dismissible fade show" role="alert">
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Fermer') }}"></button>

View file

@ -58,7 +58,7 @@
<ul class="list-unstyled">
@foreach(App\Models\Shop\Order::whereReference($contactMessage->order_number)->get() as $order)
<li class="list-inline-item"><a class="btn btn-sm btn-primary" href="{{ route('bo.orders.show', $order) }}">
{{ $order->created_at->formatLocalized('Le %d/%m/%Y à %I:%M') }}<br>
{{ $order->created_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}<br>
{{ $order->customer->lastname }} {{ $order->customer->firstname }}
</a></li>
@endforeach

View file

@ -66,7 +66,7 @@
</div>
</td>
<td>
<span>{{ $discountModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $discountModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="2000; url={{ route('order-tunnel.checkout-verify') }}">
<title>{{ config('app.name') }} - {{ __('Redirection après achat') }}</title>
<style nonce="{{ $nonce }}">
.center {
display: flex;
flex-direction: row;
justify-content: center;
}
.inline {
display: inline-block;
}
</style>
</head>
<body>
<div class="center">
<form action="{{ route('order-tunnel.checkout-verify')}}" method="GET">
<button type="submit">{{ __('Cliquez ici si vous n\'êtes pas redirigés') }}</button>
</form>
</div>
<script nonce="{{ $nonce ?? null }}">
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
window.location.href = '{{ route('order-tunnel.checkout-verify') }}';
}, 2000);
});
</script>
</body>
</html>

View file

@ -29,7 +29,7 @@
<tr>
<td>{{ $orderModel->reference }}</td>
<td>
<span>{{ $orderModel->created_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $orderModel->created_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>{{ \App\Models\Merchandise\Product::fmtCurrency($orderModel->total_ttc, '') }}</td>
<td>
@ -90,7 +90,7 @@
@endif
</td>
<td>
<span>{{ $orderModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $orderModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -14,10 +14,10 @@ $isBo = strpos(request()->route()->getPrefix(), 'bo') === 1;
<div class="col">
<span class="fw-bold">{{ __('Email') }}&nbsp;:&nbsp;{{ $order->email }}</span><br>
<small class="fst-italic">{{ __('Crée le :date', [
'date' => $order->created_at->formatLocalized('%d/%m/%Y à %I:%M')
'date' => $order->created_at->isoFormat('DD/MM/YYYY \à HH:mm')
]) }}</small><br>
<small class="fst-italic">{{ __('Mis à jour le :date', [
'date' => $order->updated_at->formatLocalized('%d/%m/%Y à %I:%M')
'date' => $order->updated_at->isoFormat('DD/MM/YYYY \à HH:mm')
]) }}</small>
@if(!$isBo)
{{-- <a class="btn btn-sm btn-info" href="{{ route('customers.orders.invoice', $order) }}" target="_blank" title="{{ __('Télécharger la facture') }}">{{ __('Facture') }}</a> --}}

View file

@ -73,7 +73,7 @@
</form>
</td>
<td>
<span>{{ $productModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $productModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -43,7 +43,7 @@
</form>
</td>
<td>
<span>{{ $rangeModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $rangeModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -34,7 +34,7 @@
{{ __('NA') }}
@endif
<td>
<span>{{ $shopModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $shopModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -74,7 +74,7 @@
</td>
@endif
<td>
<span>{{ $slideModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $slideModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -27,7 +27,7 @@
<span>{{ Str::limit(strip_tags($static_pageModel->value)) }}</span>
</td>
<td>
<span>{{ $static_pageModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
<span>{{ $static_pageModel->updated_at->isoFormat('\L\e DD/MM/YYYY \à HH:mm') }}</span>
</td>
<td>
<div class="btn-group">

View file

@ -24,6 +24,10 @@ Route::prefix('commande')->middleware(['auth:frontend', 'verified'])->name('orde
// * 4 - Recap before paiement
Route::get('/resume', [OrderController::class, 'review'])->name('review');
Route::post('/resume', [OrderController::class, 'reviewSubmit'])->name('review-submit');
// * 5 - Validate Payments
Route::get('/checkout', [OrderController::class, 'checkout'])->name('checkout');
Route::get('/checkout-verify', [OrderController::class, 'checkoutVerify'])->name('checkout-verify');
});
// * AJAX