From ca0aaaa84b759d626aa7a51d22aa0d8c9fd9ac0b Mon Sep 17 00:00:00 2001 From: kusowl Date: Wed, 25 Mar 2026 15:24:40 +0530 Subject: [PATCH] feature: implement payment verification, make cart as converted after successful payment --- .../app/Actions/MarkCartAsConvertedAction.php | 20 +++++++ .../app/Actions/MarkPaymentAsPaidAction.php | 36 +++++++++--- .../app/Actions/ProcessOrderPaymentAction.php | 4 +- .../app/Actions/VerifyStripeSessionAction.php | 57 +++++++++++++++++++ .../app/Data/VerifiedCheckoutResponseDTO.php | 41 +++++++++++++ .../Http/Controllers/PaymentController.php | 14 ++--- .../Controllers/StripeWebhookController.php | 4 +- .../Http/Requests/VerifyPaymentRequest.php | 30 ++++++++++ backend/app/Models/Order.php | 8 +++ backend/app/Providers/AppServiceProvider.php | 3 +- backend/routes/api.php | 1 + 11 files changed, 199 insertions(+), 19 deletions(-) create mode 100644 backend/app/Actions/MarkCartAsConvertedAction.php create mode 100644 backend/app/Actions/VerifyStripeSessionAction.php create mode 100644 backend/app/Data/VerifiedCheckoutResponseDTO.php create mode 100644 backend/app/Http/Requests/VerifyPaymentRequest.php diff --git a/backend/app/Actions/MarkCartAsConvertedAction.php b/backend/app/Actions/MarkCartAsConvertedAction.php new file mode 100644 index 0000000..bf409be --- /dev/null +++ b/backend/app/Actions/MarkCartAsConvertedAction.php @@ -0,0 +1,20 @@ +status = CartStatus::Converted; + $cart->save(); + + return $cart; + } +} diff --git a/backend/app/Actions/MarkPaymentAsPaidAction.php b/backend/app/Actions/MarkPaymentAsPaidAction.php index e9b9898..9baa89e 100644 --- a/backend/app/Actions/MarkPaymentAsPaidAction.php +++ b/backend/app/Actions/MarkPaymentAsPaidAction.php @@ -5,23 +5,45 @@ use App\Enums\PaymentStatusEnum; use App\Models\Payment; use App\Models\PaymentStatus; +use DB; +use Log; use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Throwable; final readonly class MarkPaymentAsPaidAction { + public function __construct(private MarkCartAsConvertedAction $cartAsConvertedAction) {} + /** * Execute the action. * - * @throws NotFoundResourceException + * @throws NotFoundResourceException|Throwable */ public function execute(Payment $payment): bool { - $status = PaymentStatus::whereName(PaymentStatusEnum::Paid->value)->value('id'); - if (! $status) { - throw new NotFoundResourceException('Paid Status not found'); - } - $payment->payment_status_id = $status; + try { - return $payment->save(); + DB::beginTransaction(); + // get the cart and make the status to converted + $cart = $payment->order->cart; + $this->cartAsConvertedAction->execute($cart); + + $status = PaymentStatus::whereName(PaymentStatusEnum::Paid->value)->value('id'); + if (! $status) { + throw new NotFoundResourceException('Paid Status not found'); + } + $payment->payment_status_id = $status; + + $payment->save(); + DB::commit(); + + return true; + + } catch (Throwable $e) { + Log::error('Cannot mark order payment as paid', [$e->getMessage()]); + DB::rollBack(); + + return false; + } } } diff --git a/backend/app/Actions/ProcessOrderPaymentAction.php b/backend/app/Actions/ProcessOrderPaymentAction.php index cd88fc9..b710ff0 100644 --- a/backend/app/Actions/ProcessOrderPaymentAction.php +++ b/backend/app/Actions/ProcessOrderPaymentAction.php @@ -14,7 +14,9 @@ final readonly class ProcessOrderPaymentAction { - public function __construct(private PaymentGatewayFactory $paymentGatewayFactory) {} + public function __construct( + private PaymentGatewayFactory $paymentGatewayFactory, + ) {} /** * Execute the action. diff --git a/backend/app/Actions/VerifyStripeSessionAction.php b/backend/app/Actions/VerifyStripeSessionAction.php new file mode 100644 index 0000000..b90e579 --- /dev/null +++ b/backend/app/Actions/VerifyStripeSessionAction.php @@ -0,0 +1,57 @@ +orders() + ->whereId($orderId) + ->first(); + if (! $order) { + throw new AccessDeniedHttpException('Order is not made by you'); + } + if (! $order->stripeSession()->where('session_id', $stripeSessionId)->exists()) { + throw new AccessDeniedHttpException('Stripe session is not made by you'); + } + + try { + $session = $this->stripe->checkout->sessions->retrieve($stripeSessionId); + } catch (ApiErrorException $e) { + Log::error('Stripe api is not available: ', [$e->getMessage()]); + throw new ServiceUnavailableHttpException('Stripe api is not available'); + } catch (Exception $e) { + throw new NotFoundHttpException('Invalid Stripe session id'); + } + + if ($session->payment_status !== 'paid' || $session->status !== 'complete') { + return VerifiedCheckoutResponseDTO::failure('Payment Unsuccessful'); + } + + return VerifiedCheckoutResponseDTO::success( + message: 'Payment Successful', + amount: $session->amount_total, + transactionId: $session->payment_intent, + mode: $session->payment_method_types[0], + ); + } +} diff --git a/backend/app/Data/VerifiedCheckoutResponseDTO.php b/backend/app/Data/VerifiedCheckoutResponseDTO.php new file mode 100644 index 0000000..9c6a4bb --- /dev/null +++ b/backend/app/Data/VerifiedCheckoutResponseDTO.php @@ -0,0 +1,41 @@ + + */ + public function toArray(): array + { + return [ + 'isSuccess' => $this->isSuccess, + 'message' => $this->message, + 'amount' => $this->amount, + 'transactionId' => $this->transactionId, + 'paymentMethod' => $this->mode, + ]; + } +} diff --git a/backend/app/Http/Controllers/PaymentController.php b/backend/app/Http/Controllers/PaymentController.php index fbab1c3..dc7ff8a 100644 --- a/backend/app/Http/Controllers/PaymentController.php +++ b/backend/app/Http/Controllers/PaymentController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers; use App\Actions\ProcessOrderPaymentAction; +use App\Actions\VerifyStripeSessionAction; use App\Enums\PaymentModes; use App\Http\Requests\PaymentRequest; +use App\Http\Requests\VerifyPaymentRequest; use App\Http\Resources\PaymentResource; use App\Models\Order; use App\Models\Payment; @@ -23,6 +25,11 @@ public function store(PaymentRequest $request, Order $order, ProcessOrderPayment return new PaymentResource($response); } + public function verify(VerifyPaymentRequest $request, VerifyStripeSessionAction $action) + { + return $action->execute($request->orderId, $request->sessionId, $request->user())->toArray(); + } + public function show(Payment $payment) { return new PaymentResource($payment); @@ -34,11 +41,4 @@ public function update(PaymentRequest $request, Payment $payment) return new PaymentResource($payment); } - - public function destroy(Payment $payment) - { - $payment->delete(); - - return response()->json(); - } } diff --git a/backend/app/Http/Controllers/StripeWebhookController.php b/backend/app/Http/Controllers/StripeWebhookController.php index 64a20a4..fca00ab 100644 --- a/backend/app/Http/Controllers/StripeWebhookController.php +++ b/backend/app/Http/Controllers/StripeWebhookController.php @@ -15,9 +15,7 @@ class StripeWebhookController extends Controller { - public function __construct(private readonly MarkPaymentAsPaidAction $paidAction) - { - } + public function __construct(private readonly MarkPaymentAsPaidAction $paidAction) {} public function __invoke(Request $request) { diff --git a/backend/app/Http/Requests/VerifyPaymentRequest.php b/backend/app/Http/Requests/VerifyPaymentRequest.php new file mode 100644 index 0000000..efaa53a --- /dev/null +++ b/backend/app/Http/Requests/VerifyPaymentRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'sessionId' => 'required|exists:stripe_sessions,session_id', + 'orderId' => 'required|exists:orders,id', + ]; + } +} diff --git a/backend/app/Models/Order.php b/backend/app/Models/Order.php index 4cfdc57..e6bd522 100644 --- a/backend/app/Models/Order.php +++ b/backend/app/Models/Order.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -44,4 +45,11 @@ public function payments(): HasMany { return $this->hasMany(Payment::class); } + + protected function totalAmount(): Attribute + { + return Attribute::make( + fn () => $this->cart?->products->sum(fn ($product) => $product->pivot->price * $product->pivot->quantity) ?? 0 + ); + } } diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 452e6b6..d5850aa 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Stripe\StripeClient; class AppServiceProvider extends ServiceProvider { @@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(StripeClient::class, fn () => new StripeClient(config('services.stripe.secret'))); } /** diff --git a/backend/routes/api.php b/backend/routes/api.php index dbfb7fb..0354caa 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -32,6 +32,7 @@ Route::apiResource('user.addresses', UserAddressController::class)->shallow(); Route::apiResource('users.orders', OrderController::class)->shallow(); Route::post('orders/{order}/payments', [PaymentController::class, 'store']); + Route::post('payments/verify', [PaymentController::class, 'verify']); }); Route::get('/categories', [ProductCategoryController::class, 'index']); Route::apiResource('products', ProductController::class);