diff --git a/backend/app/Actions/ProcessOrderPaymentAction.php b/backend/app/Actions/ProcessOrderPaymentAction.php new file mode 100644 index 0000000..cd88fc9 --- /dev/null +++ b/backend/app/Actions/ProcessOrderPaymentAction.php @@ -0,0 +1,49 @@ +paymentGatewayFactory->make($mode); + try { + DB::beginTransaction(); + $response = $gateway->charge($order); + + if ($response->isSuccess) { + $order->payments()->create( + [ + 'transaction_id' => $response->transactionId, + 'amount' => $response->amount, + 'currency' => $response->currency, + 'payment_method' => $response->method->value, + 'payment_status_id' => PaymentStatus::getIdByName(PaymentStatusEnum::Unpaid->value), + ] + ); + } + DB::commit(); + + return $response; + } catch (Throwable $e) { + DB::rollBack(); + Log::error('Error occurred while processing the payment.', [$e->getMessage()]); + abort(500, 'Something went wrong. Please try again or contact us to get in touch with our support team. '); + } + } +} diff --git a/backend/app/Contracts/PaymentGateway.php b/backend/app/Contracts/PaymentGateway.php new file mode 100644 index 0000000..43ce900 --- /dev/null +++ b/backend/app/Contracts/PaymentGateway.php @@ -0,0 +1,11 @@ + + */ + public function toArray(): array + { + return [ + 'isSuccess' => $this->isSuccess, + 'transactionId' => $this->transactionId, + 'amount' => $this->amount, + 'currency' => $this->currency, + 'method' => $this->method, + 'redirectUrl' => $this->redirectUrl, + 'errorMessage' => $this->errorMessage, + ]; + } +} diff --git a/backend/app/Enums/PaymentModes.php b/backend/app/Enums/PaymentModes.php new file mode 100644 index 0000000..4bd844d --- /dev/null +++ b/backend/app/Enums/PaymentModes.php @@ -0,0 +1,9 @@ +execute($order, PaymentModes::tryFrom($request->mode)); + + return new PaymentResource($response); + } + + public function show(Payment $payment) + { + return new PaymentResource($payment); + } + + public function update(PaymentRequest $request, Payment $payment) + { + $payment->update($request->validated()); + + return new PaymentResource($payment); + } + + public function destroy(Payment $payment) + { + $payment->delete(); + + return response()->json(); + } +} diff --git a/backend/app/Http/Requests/PaymentRequest.php b/backend/app/Http/Requests/PaymentRequest.php new file mode 100644 index 0000000..9b0990f --- /dev/null +++ b/backend/app/Http/Requests/PaymentRequest.php @@ -0,0 +1,22 @@ + ['required', Rule::enum(PaymentModes::class)], + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/backend/app/Http/Resources/PaymentResource.php b/backend/app/Http/Resources/PaymentResource.php new file mode 100644 index 0000000..0f18bc1 --- /dev/null +++ b/backend/app/Http/Resources/PaymentResource.php @@ -0,0 +1,30 @@ + $this->resource->isSuccess, + 'amount' => $this->resource->amount, + 'currency' => $this->resource->currency, + 'method' => $this->resource->method, + 'redirectUrl' => $this->resource->redirectUrl, + 'errorMessage' => $this->resource->errorMessage, + ]; + } +} diff --git a/backend/app/Models/Payment.php b/backend/app/Models/Payment.php new file mode 100644 index 0000000..64979f0 --- /dev/null +++ b/backend/app/Models/Payment.php @@ -0,0 +1,40 @@ +belongsTo(Order::class); + } + + public function paymentStatus(): BelongsTo + { + return $this->belongsTo(PaymentStatus::class); + } + + protected function status(): Attribute + { + return Attribute::make(fn () => $this->paymentStatus?->name); + } +} diff --git a/backend/app/Models/PaymentStatus.php b/backend/app/Models/PaymentStatus.php new file mode 100644 index 0000000..6095058 --- /dev/null +++ b/backend/app/Models/PaymentStatus.php @@ -0,0 +1,22 @@ +value('id'); + } +} diff --git a/backend/app/Services/Payment/CodPaymentGateway.php b/backend/app/Services/Payment/CodPaymentGateway.php new file mode 100644 index 0000000..214450f --- /dev/null +++ b/backend/app/Services/Payment/CodPaymentGateway.php @@ -0,0 +1,14 @@ + new CodPaymentGateway, + PaymentModes::StripeCheckout => new StripePaymentGateway, + }; + } +} diff --git a/backend/app/Services/Payment/StripePaymentGateway.php b/backend/app/Services/Payment/StripePaymentGateway.php new file mode 100644 index 0000000..65fe097 --- /dev/null +++ b/backend/app/Services/Payment/StripePaymentGateway.php @@ -0,0 +1,83 @@ +cart()->withProducts()->first(); + + $totalAmount = 0; + $lineItems = []; + + foreach ($cart->products as $product) { + $priceInCents = (int) ($product->pivot->price * 100); + + $totalAmount += ($priceInCents * $product->pivot->quantity); + + $lineItems[] = new StripeLineItemDTO( + currency: StripeCurrency::INR, + price: $priceInCents, + productName: $product->title, + productDescription: $product->description, + quantity: $product->pivot->quantity, + ); + } + + $currency = StripeCurrency::INR->value; + + try { + $data = new StripeSessionDataDTO( + lineItems: $lineItems, + mode: StripePaymentMode::Payment, + successUrl: config('app.frontend_url').'/order/payment/success?session_id={CHECKOUT_SESSION_ID}"', + cancelUrl: config('app.frontend_url').'/order/payment/cancel', + ); + $session = Session::create($data->toArray()); + + // add stripe session to order + $order->stripeSession()->create( + [ + 'session_id' => $session->id, + ] + ); + + return PaymentResponseDTO::success( + transactionId: $session->id, + amount: $totalAmount, + currency: $currency, + method: PaymentModes::StripeCheckout, + redirectUrl: $session->url, + ); + + } catch (Exception $e) { + Log::error('Stripe Checkout session cannot be created.', ['error' => $e->getMessage()]); + + return PaymentResponseDTO::failure( + amount: $totalAmount, + currency: $currency, + method: PaymentModes::StripeCheckout, + errorMessage: 'Payment gateway unavailable. Please try again.' + ); + } + } +} diff --git a/backend/database/migrations/2026_03_22_134921_create_payments_table.php b/backend/database/migrations/2026_03_22_134921_create_payments_table.php new file mode 100644 index 0000000..05a15e0 --- /dev/null +++ b/backend/database/migrations/2026_03_22_134921_create_payments_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('order_id'); + $table->string('transaction_id'); + $table->integer('amount'); + $table->string('currency'); + $table->string('payment_method'); + $table->foreignIdFor(PaymentStatus::class); + $table->timestamps(); + $table->unique(['transaction_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('payments'); + } +}; diff --git a/backend/database/migrations/2026_03_23_044430_create_payment_statuses_table.php b/backend/database/migrations/2026_03_23_044430_create_payment_statuses_table.php new file mode 100644 index 0000000..c321b71 --- /dev/null +++ b/backend/database/migrations/2026_03_23_044430_create_payment_statuses_table.php @@ -0,0 +1,21 @@ +id(); + $table->string('name'); + }); + } + + public function down(): void + { + Schema::dropIfExists('payment_statuses'); + } +}; diff --git a/backend/database/seeders/PaymentStatusSeeder.php b/backend/database/seeders/PaymentStatusSeeder.php new file mode 100644 index 0000000..ab68c84 --- /dev/null +++ b/backend/database/seeders/PaymentStatusSeeder.php @@ -0,0 +1,20 @@ + $status->value, + ]; + }, PaymentStatusEnum::cases()); + PaymentStatus::insertOrIgnore($data); + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index b92250a..139acd8 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -4,6 +4,7 @@ use App\Http\Controllers\CartController; use App\Http\Controllers\FavouriteProductController; use App\Http\Controllers\OrderController; +use App\Http\Controllers\PaymentController; use App\Http\Controllers\ProductCategoryController; use App\Http\Controllers\ProductController; use App\Http\Controllers\ProductImagesController; @@ -29,6 +30,7 @@ Route::apiResource('user.addresses', UserAddressController::class)->shallow(); Route::apiResource('users.orders', OrderController::class)->shallow(); + Route::post('orders/{order}/payments', [PaymentController::class, 'store']); }); Route::get('/categories', [ProductCategoryController::class, 'index']); Route::apiResource('products', ProductController::class);