From b757d7df9d87f1c6d41f4bd4bfb9845a0619cbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Wed, 8 Mar 2023 18:01:09 +0100 Subject: [PATCH] Payment system PART2 : Stripe OK SYNC 46 - changed order pk column to id - refactored and fixed inShop - using php Money for transaction --- app/Enums/Order/OrderProcess.php | 2 +- app/Enums/Order/PaymentModeEnum.php | 24 +- app/Enums/Order/TransactionStatus.php | 2 +- app/Enums/Stripe/CheckoutPaymentStatus.php | 14 + app/Http/Controllers/Bo/OrderController.php | 5 +- .../Controllers/Fo/CustomerController.php | 2 +- app/Http/Controllers/Fo/OrderController.php | 323 ++++++++++++++++-- .../Adapters/InShopPaymentAdapter.php | 50 ++- .../Adapters/StripePaymentAdapter.php | 127 ++++++- .../Adapters/SystemPayPaymentAdapter.php | 16 +- app/Lib/Payments/PaymentAdapter.php | 46 ++- app/Models/Customer/Customer.php | 9 + app/Models/Payment/Transaction.php | 74 +++- app/Models/Shop/Order.php | 87 +++-- app/Models/Shop/Timeslot.php | 2 +- app/Providers/AppServiceProvider.php | 14 + composer.json | 1 + ...9_05_03_000001_create_customer_columns.php | 35 ++ ...000003_create_subscription_items_table.php | 38 +++ ...94853_change_order_pk_name_to_standard.php | 38 +++ ...03_02_093747_create_transactions_table.php | 6 +- resources/theme | 2 +- .../views/modules/flashMessage.blade.php | 8 +- resources/views/pages/contacts/show.blade.php | 2 +- .../views/pages/discounts/indexList.blade.php | 2 +- .../pages/order-tunnel-checkout.blade.php | 34 ++ .../views/pages/orders/indexList.blade.php | 4 +- .../orders/parts/display-order.blade.php | 4 +- .../views/pages/products/indexList.blade.php | 2 +- .../views/pages/ranges/indexList.blade.php | 2 +- .../views/pages/shops/indexList.blade.php | 2 +- .../views/pages/slides/indexList.blade.php | 2 +- .../pages/static_pages/indexList.blade.php | 2 +- routes/web-shop.php | 4 + 34 files changed, 869 insertions(+), 116 deletions(-) create mode 100644 app/Enums/Stripe/CheckoutPaymentStatus.php create mode 100644 database/migrations/2019_05_03_000001_create_customer_columns.php create mode 100644 database/migrations/2019_05_03_000003_create_subscription_items_table.php create mode 100644 database/migrations/2023_03_01_094853_change_order_pk_name_to_standard.php create mode 100644 resources/views/pages/order-tunnel-checkout.blade.php diff --git a/app/Enums/Order/OrderProcess.php b/app/Enums/Order/OrderProcess.php index 88f09b6..534c29d 100644 --- a/app/Enums/Order/OrderProcess.php +++ b/app/Enums/Order/OrderProcess.php @@ -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 diff --git a/app/Enums/Order/PaymentModeEnum.php b/app/Enums/Order/PaymentModeEnum.php index bce2e0c..78d8023 100644 --- a/app/Enums/Order/PaymentModeEnum.php +++ b/app/Enums/Order/PaymentModeEnum.php @@ -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); } } diff --git a/app/Enums/Order/TransactionStatus.php b/app/Enums/Order/TransactionStatus.php index 31efdb7..649acc0 100644 --- a/app/Enums/Order/TransactionStatus.php +++ b/app/Enums/Order/TransactionStatus.php @@ -38,7 +38,7 @@ final class TransactionStatus extends BaseEnumRoutable return [ 'pending' => __('En attente'), 'failed' => __('Échec'), - 'payed' => __('Validée') + 'validated' => __('Validée') ]; } } diff --git a/app/Enums/Stripe/CheckoutPaymentStatus.php b/app/Enums/Stripe/CheckoutPaymentStatus.php new file mode 100644 index 0000000..335e541 --- /dev/null +++ b/app/Enums/Stripe/CheckoutPaymentStatus.php @@ -0,0 +1,14 @@ +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(); diff --git a/app/Http/Controllers/Fo/CustomerController.php b/app/Http/Controllers/Fo/CustomerController.php index 07fd9a7..2d51f72 100644 --- a/app/Http/Controllers/Fo/CustomerController.php +++ b/app/Http/Controllers/Fo/CustomerController.php @@ -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')); } diff --git a/app/Http/Controllers/Fo/OrderController.php b/app/Http/Controllers/Fo/OrderController.php index ce65064..32f190d 100644 --- a/app/Http/Controllers/Fo/OrderController.php +++ b/app/Http/Controllers/Fo/OrderController.php @@ -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())) { diff --git a/app/Lib/Payments/Adapters/InShopPaymentAdapter.php b/app/Lib/Payments/Adapters/InShopPaymentAdapter.php index 807f65f..d00564a 100644 --- a/app/Lib/Payments/Adapters/InShopPaymentAdapter.php +++ b/app/Lib/Payments/Adapters/InShopPaymentAdapter.php @@ -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 + { + } } diff --git a/app/Lib/Payments/Adapters/StripePaymentAdapter.php b/app/Lib/Payments/Adapters/StripePaymentAdapter.php index 42227d1..dde7b89 100644 --- a/app/Lib/Payments/Adapters/StripePaymentAdapter.php +++ b/app/Lib/Payments/Adapters/StripePaymentAdapter.php @@ -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(); + } } diff --git a/app/Lib/Payments/Adapters/SystemPayPaymentAdapter.php b/app/Lib/Payments/Adapters/SystemPayPaymentAdapter.php index fa806d4..1c55ad5 100644 --- a/app/Lib/Payments/Adapters/SystemPayPaymentAdapter.php +++ b/app/Lib/Payments/Adapters/SystemPayPaymentAdapter.php @@ -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; + } } diff --git a/app/Lib/Payments/PaymentAdapter.php b/app/Lib/Payments/PaymentAdapter.php index 429f445..aa272c2 100644 --- a/app/Lib/Payments/PaymentAdapter.php +++ b/app/Lib/Payments/PaymentAdapter.php @@ -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; } diff --git a/app/Models/Customer/Customer.php b/app/Models/Customer/Customer.php index 2a5ff3a..a901fb2 100644 --- a/app/Models/Customer/Customer.php +++ b/app/Models/Customer/Customer.php @@ -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'); }); } diff --git a/app/Models/Payment/Transaction.php b/app/Models/Payment/Transaction.php index a9e32f3..f11d393 100644 --- a/app/Models/Payment/Transaction.php +++ b/app/Models/Payment/Transaction.php @@ -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 * diff --git a/app/Models/Shop/Order.php b/app/Models/Shop/Order.php index a0542e0..e42359a 100644 --- a/app/Models/Shop/Order.php +++ b/app/Models/Shop/Order.php @@ -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; diff --git a/app/Models/Shop/Timeslot.php b/app/Models/Shop/Timeslot.php index 0473bcb..74ea5cd 100644 --- a/app/Models/Shop/Timeslot.php +++ b/app/Models/Shop/Timeslot.php @@ -36,7 +36,7 @@ class Timeslot extends Model */ public function order() { - return $this->belongsTo(Order::class, 'order_uuid', 'uuid'); + return $this->belongsTo(Order::class); } /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 27d8ade..571b191 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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'); diff --git a/composer.json b/composer.json index 24e7d12..f329e18 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/database/migrations/2019_05_03_000001_create_customer_columns.php b/database/migrations/2019_05_03_000001_create_customer_columns.php new file mode 100644 index 0000000..2da0dc0 --- /dev/null +++ b/database/migrations/2019_05_03_000001_create_customer_columns.php @@ -0,0 +1,35 @@ +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', + ]); + }); + } +}; diff --git a/database/migrations/2019_05_03_000003_create_subscription_items_table.php b/database/migrations/2019_05_03_000003_create_subscription_items_table.php new file mode 100644 index 0000000..67fcf3f --- /dev/null +++ b/database/migrations/2019_05_03_000003_create_subscription_items_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/migrations/2023_03_01_094853_change_order_pk_name_to_standard.php b/database/migrations/2023_03_01_094853_change_order_pk_name_to_standard.php new file mode 100644 index 0000000..c5464a5 --- /dev/null +++ b/database/migrations/2023_03_01_094853_change_order_pk_name_to_standard.php @@ -0,0 +1,38 @@ +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'); + }); + } +} diff --git a/database/migrations/2023_03_02_093747_create_transactions_table.php b/database/migrations/2023_03_02_093747_create_transactions_table.php index 14d199c..4d81d49 100644 --- a/database/migrations/2023_03_02_093747_create_transactions_table.php +++ b/database/migrations/2023_03_02_093747_create_transactions_table.php @@ -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(); }); diff --git a/resources/theme b/resources/theme index caa09ad..042cb97 160000 --- a/resources/theme +++ b/resources/theme @@ -1 +1 @@ -Subproject commit caa09adba36f08a9cd0033260430b8197835a3c9 +Subproject commit 042cb97947ebd020a869556861e75f81e8b34ee9 diff --git a/resources/views/modules/flashMessage.blade.php b/resources/views/modules/flashMessage.blade.php index fd3648b..6a499dc 100644 --- a/resources/views/modules/flashMessage.blade.php +++ b/resources/views/modules/flashMessage.blade.php @@ -1,12 +1,12 @@ @if(!request()->routeIs('login')) - @if ($message = Session::get('success')) + @if ($message = Session::get('success', Session::get('bo.success'))) @endif - @if ($message = Session::get('error')) + @if ($message = Session::get('error', Session::get('bo.error')))