From 8154dff0468da4320e7943bd5cb33a1170a1b77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2E=20Nagy=20Gerg=C5=91?= Date: Sat, 13 Jan 2024 22:45:21 +0100 Subject: [PATCH] rework gateway --- config/bazar.php | 8 +- routes/web.php | 8 ++ src/BazarServiceProvider.php | 21 ++- src/Cart/Driver.php | 4 +- src/Events/CheckoutFailed.php | 30 +++++ src/Events/CheckoutProcessed.php | 8 +- src/Events/PaymentCaptureFailed.php | 30 +++++ src/Events/PaymentCaptured.php | 30 +++++ src/Gateway/CashDriver.php | 30 +---- src/Gateway/Driver.php | 146 ++++++++++++++++++++- src/Gateway/Response.php | 42 +++--- src/Gateway/TransferDriver.php | 17 --- src/Http/Controllers/Controller.php | 10 ++ src/Http/Controllers/GatewayController.php | 27 ++++ src/Listeners/PlaceOrder.php | 4 +- src/Listeners/RefreshInventory.php | 4 +- src/Support/Facades/Gateway.php | 1 + 17 files changed, 342 insertions(+), 78 deletions(-) create mode 100644 routes/web.php create mode 100644 src/Events/CheckoutFailed.php create mode 100644 src/Events/PaymentCaptureFailed.php create mode 100644 src/Events/PaymentCaptured.php create mode 100644 src/Http/Controllers/Controller.php create mode 100644 src/Http/Controllers/GatewayController.php diff --git a/config/bazar.php b/config/bazar.php index c14599c3..953bd15c 100644 --- a/config/bazar.php +++ b/config/bazar.php @@ -57,12 +57,12 @@ 'default' => env('BAZAR_GATEWAY_DRIVER', 'transfer'), 'drivers' => [ 'cash' => [ - 'success_url' => '/', - 'failed_url' => '/', + 'success_url' => '/?order={order}&status=success', + 'failure_url' => '/?order={order}&status=failed', ], 'transfer' => [ - 'success_url' => '/', - 'failed_url' => '/', + 'success_url' => '/?order={order}&status=success', + 'failure_url' => '/?order={order}&status=failed', ], ], ], diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 00000000..2db7d09f --- /dev/null +++ b/routes/web.php @@ -0,0 +1,8 @@ +name('gateway.capture'); +Route::any('/gateway/{driver}/notification', [GatewayController::class, 'notification'])->name('gateway.notification'); diff --git a/src/BazarServiceProvider.php b/src/BazarServiceProvider.php index 5d1eb547..020568f8 100644 --- a/src/BazarServiceProvider.php +++ b/src/BazarServiceProvider.php @@ -56,10 +56,27 @@ public function boot(): void $this->registerPublishes(); } + if (! $this->app->routesAreCached()) { + $this->registerRoutes(); + } + $this->registerEvents(); $this->registerViews(); } + /** + * Register routes. + */ + protected function registerRoutes(): void + { + $this->app['router'] + ->prefix('bazar') + ->as('bazar.') + ->group(function (): void { + $this->loadRoutesFrom(__DIR__.'/../routes/auth.php'); + }); + } + /** * Register views. */ @@ -103,7 +120,7 @@ protected function registerCommands(): void protected function registerEvents(): void { $this->app['events']->listen(Logout::class, Listeners\ClearCookies::class); - $this->app['events']->listen(Events\CheckoutProcessed::class, Listeners\PlaceOrder::class); - $this->app['events']->listen(Events\CheckoutProcessed::class, Listeners\RefreshInventory::class); + $this->app['events']->listen(Events\PaymentCaptured::class, Listeners\PlaceOrder::class); + $this->app['events']->listen(Events\PaymentCaptured::class, Listeners\RefreshInventory::class); } } diff --git a/src/Cart/Driver.php b/src/Cart/Driver.php index d3275494..4686dc79 100644 --- a/src/Cart/Driver.php +++ b/src/Cart/Driver.php @@ -250,8 +250,8 @@ public function isEmpty(): bool */ public function checkout(string $driver): Response { - return App::call(function (Request $request) use ($driver): Response { - return Gateway::driver($driver)->checkout($request, $this->getModel()->toOrder()); + return App::call(static function (Request $request) use ($driver): Response { + return Gateway::driver($driver)->handleCheckout($request); }); } diff --git a/src/Events/CheckoutFailed.php b/src/Events/CheckoutFailed.php new file mode 100644 index 00000000..c972e515 --- /dev/null +++ b/src/Events/CheckoutFailed.php @@ -0,0 +1,30 @@ +driver = $driver; + $this->order = $order; + } +} diff --git a/src/Events/CheckoutProcessed.php b/src/Events/CheckoutProcessed.php index 3ccd8ba0..c9faaea9 100644 --- a/src/Events/CheckoutProcessed.php +++ b/src/Events/CheckoutProcessed.php @@ -14,11 +14,17 @@ class CheckoutProcessed */ public Order $order; + /** + * The gateway driver. + */ + public string $driver; + /** * Create a new event instance. */ - public function __construct(Order $order) + public function __construct(string $driver, Order $order) { + $this->driver = $driver; $this->order = $order; } } diff --git a/src/Events/PaymentCaptureFailed.php b/src/Events/PaymentCaptureFailed.php new file mode 100644 index 00000000..98a285ec --- /dev/null +++ b/src/Events/PaymentCaptureFailed.php @@ -0,0 +1,30 @@ +driver = $driver; + $this->order = $order; + } +} diff --git a/src/Events/PaymentCaptured.php b/src/Events/PaymentCaptured.php new file mode 100644 index 00000000..16e02b20 --- /dev/null +++ b/src/Events/PaymentCaptured.php @@ -0,0 +1,30 @@ +driver = $driver; + $this->order = $order; + } +} diff --git a/src/Gateway/CashDriver.php b/src/Gateway/CashDriver.php index 2a03a684..75f17708 100644 --- a/src/Gateway/CashDriver.php +++ b/src/Gateway/CashDriver.php @@ -2,10 +2,8 @@ namespace Cone\Bazar\Gateway; -use Cone\Bazar\Events\CheckoutProcessed; use Cone\Bazar\Models\Order; use Cone\Bazar\Models\Transaction; -use Illuminate\Http\Request; class CashDriver extends Driver { @@ -19,13 +17,9 @@ class CashDriver extends Driver */ public function pay(Order $order, ?float $amount = null, array $attributes = []): Transaction { - $transaction = parent::pay($order, $amount, array_merge($attributes, [ + return parent::pay($order, $amount, array_merge($attributes, [ 'completed_at' => time(), ])); - - $order->markAs(Order::PENDING); - - return $transaction; } /** @@ -33,28 +27,8 @@ public function pay(Order $order, ?float $amount = null, array $attributes = []) */ public function refund(Order $order, ?float $amount = null, array $attributes = []): Transaction { - $transaction = parent::refund($order, $amount, array_merge($attributes, [ + return parent::refund($order, $amount, array_merge($attributes, [ 'completed_at' => time(), ])); - - $order->markAs(Order::REFUNDED); - - return $transaction; - } - - /** - * Handle the checkout request. - */ - public function checkout(Request $request, Order $order): Response - { - $response = parent::checkout($request, $order); - - $url = $order->status === Order::PENDING - ? $this->config['success_url'] - : $this->config['failed_url']; - - CheckoutProcessed::dispatch($order); - - return $response->url($url); } } diff --git a/src/Gateway/Driver.php b/src/Gateway/Driver.php index b859dacb..e776be47 100644 --- a/src/Gateway/Driver.php +++ b/src/Gateway/Driver.php @@ -2,10 +2,17 @@ namespace Cone\Bazar\Gateway; +use Cone\Bazar\Events\CheckoutFailed; +use Cone\Bazar\Events\CheckoutProcessed; +use Cone\Bazar\Events\PaymentCaptured; +use Cone\Bazar\Events\PaymentCaptureFailed; use Cone\Bazar\Models\Order; use Cone\Bazar\Models\Transaction; use Cone\Bazar\Support\Driver as BaseDriver; +use Cone\Bazar\Support\Facades\Cart; use Illuminate\Http\Request; +use Illuminate\Http\Response as HttpResponse; +use Illuminate\Support\Facades\URL; use Throwable; abstract class Driver extends BaseDriver @@ -28,7 +35,11 @@ public function pay(Order $order, ?float $amount = null, array $attributes = []) */ public function refund(Order $order, ?float $amount = null, array $attributes = []): Transaction { - return $order->refund($amount, $this->name, $attributes); + $transaction = $order->refund($amount, $this->name, $attributes); + + $order->markAs(Order::REFUNDED); + + return $transaction; } /** @@ -39,17 +50,142 @@ public function getTransactionUrl(Transaction $transaction): ?string return null; } + /** + * Get the URL for the payment capture. + */ + public function getCaptureUrl(Order $order): string + { + return URL::route('bazar.gateway.capture', [ + 'driver' => $this->name, + 'order' => $order->uuid, + ]); + } + + /** + * Get the success URL. + */ + public function getSuccessUrl(Order $order): string + { + return str_replace(['{order}'], [$order->uuid], $this->config['success_url']); + } + + /** + * Get the failure URL. + */ + public function getFailureUrl(Order $order): string + { + return str_replace(['{order}'], [$order->uuid], $this->config['failure_url']); + } + + /** + * Resolve the order model for checkout. + */ + public function resolveOrderForCheckout(Request $request): Order + { + return Cart::getModel()->toOrder(); + } + /** * Handle the checkout request. */ - public function checkout(Request $request, Order $order): Response + public function handleCheckout(Request $request): Response + { + $order = $this->resolveOrderForCheckout($request); + + try { + $this->checkout($request, $order); + + CheckoutProcessed::dispatch($this->name, $order); + + $url = $this->getCaptureUrl($order); + } catch (Throwable $exception) { + report($exception); + + CheckoutFailed::dispatch($this->name, $order); + + $url = $this->getFailureUrl($order); + } + + return new Response($url, $order->toArray()); + } + + /** + * Checkout the order. + */ + public function checkout(Request $request, Order $order): Order + { + return $order; + } + + /** + * Resolve the order model for payment capture. + */ + public function resolveOrderForCapture(Request $request): Order + { + return Order::proxy()->newQuery()->where('uuid', $request->input('order'))->firstOrFail(); + } + + /** + * Handle the capture request. + */ + public function handleCapture(Request $request): Response { + $order = $this->resolveOrderForCapture($request); + try { - $this->pay($order); + $this->capture($request, $order); + + PaymentCaptured::dispatch($this->name, $order); + + $url = $this->getSuccessUrl($order); } catch (Throwable $exception) { - $order->markAs(Order::FAILED); + report($exception); + + PaymentCaptureFailed::dispatch($this->name, $order); + + $url = $this->getFailureUrl($order); } - return new Response($order); + return new Response($url, $order->toArray()); + } + + /** + * Capture the payment. + */ + public function capture(Request $request, Order $order): Order + { + $this->pay($order); + + return $order; + } + + /** + * Resolve the order model for notification. + */ + public function resolveOrderForNotification(Request $request): Order + { + return Order::proxy()->newQuery()->where('uuid', $request->input('order'))->firstOrFail(); + } + + /** + * Handle the notification request. + */ + public function handleNotification(Request $request): Response + { + $order = $this->resolveOrderForNotification($request); + + $this->notification($request, $order); + + return (new Response())->respondWith(function (string $url, array $data): HttpResponse { + return new HttpResponse('', HttpResponse::HTTP_NO_CONTENT); + }); + } + + /** + * Update the order update after the notification. + */ + public function notification(Request $request, Order $order): Order + { + return $order; } } diff --git a/src/Gateway/Response.php b/src/Gateway/Response.php index af359364..e5aa6fdf 100644 --- a/src/Gateway/Response.php +++ b/src/Gateway/Response.php @@ -2,7 +2,7 @@ namespace Cone\Bazar\Gateway; -use Cone\Bazar\Models\Order; +use Closure; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\JsonResponse; @@ -11,11 +11,6 @@ class Response implements Arrayable, Responsable { - /** - * The order instance. - */ - protected Order $order; - /** * The redirect URL. */ @@ -26,12 +21,16 @@ class Response implements Arrayable, Responsable */ protected array $data = []; + /** + * The response resolver. + */ + protected ?Closure $responseResolver = null; + /** * Create a new response instance. */ - public function __construct(Order $order, string $url = '/', array $data = []) + public function __construct(string $url = '/', array $data = []) { - $this->order = $order; $this->url = $url; $this->data = $data; } @@ -47,11 +46,21 @@ public function url(string $url): static } /** - * Merge the response data. + * Set the response data. */ - public function with(array $data): static + public function data(array $data): static { - $this->data = array_merge($this->data, $data); + $this->data = $data; + + return $this; + } + + /** + * Create a custom response. + */ + public function respondWith(Closure $callback): static + { + $this->responseResolver = $callback; return $this; } @@ -63,7 +72,6 @@ public function toArray(): array { return array_merge($this->data, [ 'url' => $this->url, - 'status' => $this->order->status, ]); } @@ -72,8 +80,12 @@ public function toArray(): array */ public function toResponse($request): BaseResponse { - return $request->expectsJson() - ? new JsonResponse($this->toArray()) - : new RedirectResponse($this->url); + if (! is_null($this->responseResolver)) { + return call_user_func_array($this->responseResolver, [$this->url, $this->data]); + } elseif ($request->wantsJson()) { + new JsonResponse($this->toArray()); + } + + new RedirectResponse($this->url); } } diff --git a/src/Gateway/TransferDriver.php b/src/Gateway/TransferDriver.php index 03faae81..ca25c8cd 100644 --- a/src/Gateway/TransferDriver.php +++ b/src/Gateway/TransferDriver.php @@ -2,27 +2,10 @@ namespace Cone\Bazar\Gateway; -use Cone\Bazar\Models\Order; -use Illuminate\Http\Request; - class TransferDriver extends Driver { /** * The driver name. */ protected string $name = 'transfer'; - - /** - * Handle the checkout request. - */ - public function checkout(Request $request, Order $order): Response - { - $response = parent::checkout($request, $order); - - $url = $order->status === Order::PENDING - ? $this->config['success_url'] - : $this->config['failed_url']; - - return $response->url($url); - } } diff --git a/src/Http/Controllers/Controller.php b/src/Http/Controllers/Controller.php new file mode 100644 index 00000000..470a3134 --- /dev/null +++ b/src/Http/Controllers/Controller.php @@ -0,0 +1,10 @@ +handleCapture($request)->toResponse($request); + } + + /** + * Handle the notification requrest. + */ + public function notification(Request $request, string $driver): Response + { + return Gateway::driver($driver)->handleNotification($request)->toResponse($request); + } +} + diff --git a/src/Listeners/PlaceOrder.php b/src/Listeners/PlaceOrder.php index b61a866d..89113bdc 100644 --- a/src/Listeners/PlaceOrder.php +++ b/src/Listeners/PlaceOrder.php @@ -2,7 +2,7 @@ namespace Cone\Bazar\Listeners; -use Cone\Bazar\Events\CheckoutProcessed; +use Cone\Bazar\Events\PaymentCaptured; use Cone\Bazar\Models\Order; class PlaceOrder @@ -10,7 +10,7 @@ class PlaceOrder /** * Handle the event. */ - public function handle(CheckoutProcessed $event): void + public function handle(PaymentCaptured $event): void { $event->order->markAs(Order::IN_PROGRESS); diff --git a/src/Listeners/RefreshInventory.php b/src/Listeners/RefreshInventory.php index 68ea66fd..dbebce7e 100644 --- a/src/Listeners/RefreshInventory.php +++ b/src/Listeners/RefreshInventory.php @@ -2,7 +2,7 @@ namespace Cone\Bazar\Listeners; -use Cone\Bazar\Events\CheckoutProcessed; +use Cone\Bazar\Events\PaymentCaptured; use Cone\Bazar\Interfaces\Stockable; use Cone\Bazar\Models\Item; @@ -11,7 +11,7 @@ class RefreshInventory /** * Handle the event. */ - public function handle(CheckoutProcessed $event): void + public function handle(PaymentCaptured $event): void { $event->order->loadMissing(['items', 'items.buyable']); diff --git a/src/Support/Facades/Gateway.php b/src/Support/Facades/Gateway.php index e5ba804f..78d8334d 100644 --- a/src/Support/Facades/Gateway.php +++ b/src/Support/Facades/Gateway.php @@ -7,6 +7,7 @@ /** * @method static array getAvailableDrivers(\Cone\Bazar\Interfaces\Itemable $model) + * @method static \Cone\Bazar\Gateway\Driver driver(string $driver) * * @see \Cone\Bazar\Interfaces\Gateway\Manager */