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:
parent
3906c96a6e
commit
b757d7df9d
34 changed files with 869 additions and 116 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ final class TransactionStatus extends BaseEnumRoutable
|
|||
return [
|
||||
'pending' => __('En attente'),
|
||||
'failed' => __('Échec'),
|
||||
'payed' => __('Validée')
|
||||
'validated' => __('Validée')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
app/Enums/Stripe/CheckoutPaymentStatus.php
Normal file
14
app/Enums/Stripe/CheckoutPaymentStatus.php
Normal 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
|
||||
{
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Timeslot extends Model
|
|||
*/
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class, 'order_uuid', 'uuid');
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
34
resources/views/pages/order-tunnel-checkout.blade.php
Normal file
34
resources/views/pages/order-tunnel-checkout.blade.php
Normal 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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ $isBo = strpos(request()->route()->getPrefix(), 'bo') === 1;
|
|||
<div class="col">
|
||||
<span class="fw-bold">{{ __('Email') }} : {{ $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> --}}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue