TASKS 17: Payment Gateway Integration - RadLeoOFC/laravel-admin-panel GitHub Wiki

Payment Gateway Integration Report

Introduction

This report documents the integration of the Stripe payment gateway into the Laravel project located at C:\xampp\htdocs\internship-project. The implementation was carried out in a local development environment without using real accounts or credit cards.

The goal of this task was to allow users to pay for their memberships or desk bookings via Stripe. The following steps were taken:


1. Installation of Stripe SDK

The Stripe PHP SDK was installed using Composer. The following command was executed:

composer require stripe/stripe-php

Screenshot: Successful installation of Stripe SDK.

Successful installation of Stripe SDK


2. Database Setup

Creating the payments Table

A new migration was created and the following schema was defined:

Schema::create('payments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->foreignId('membership_id')->constrained()->onDelete('cascade');
    $table->decimal('amount', 10, 2);
    $table->enum('status', ['pending', 'paid', 'failed'])->default('pending');
    $table->string('payment_method')->nullable();
    $table->string('transaction_reference')->nullable();
    $table->json('gateway_response')->nullable();
    $table->timestamps();
});

Screenshot: Migration file for the payments table.

Migration file for the 'payments' table

The migration was executed using:

php artisan migrate

Screenshot: Successful execution of the migration.

Successful execution of the migration


3. Model Creation

A model Payment was created to interact with the payments table. The model defines relationships with the User and Membership models.

class Payment extends Model {
    use HasFactory;

    protected $fillable = ['user_id', 'membership_id', 'amount', 'status', 'payment_method', 'transaction_reference', 'gateway_response'];

    public function user() {
        return $this->belongsTo(User::class);
    }

    public function membership() {
        return $this->belongsTo(Membership::class);
    }
}

Screenshot: Payment model file (Payment.php).

Payment model file (Payment.php)


4. Payment Controller

A controller PaymentController was created to handle payment processing. It includes methods for displaying the checkout form, processing payments via Stripe, and handling payment success.

    // Processing payment via Stripe Elements
    public function processPayment(Request $request) {
        Stripe::setApiKey(env('STRIPE_SECRET'));
    
        try {
            // Retrieve membership data
            $membership = Membership::findOrFail($request->membership_id);
    
            // Calculate the amount due
            $amount_due = max($membership->price - $membership->amount_paid, 0);
    
            // Ensure the amount is greater than 0
            if ($amount_due <= 0) {
                return response()->json(['success' => false, 'message' => 'No payment required. Membership is already fully paid.']);
            }
    
            // Create a PaymentIntent with the required amount
            $paymentIntent = \Stripe\PaymentIntent::create([
                'amount' => $amount_due * 100, // Amount in cents
                'currency' => 'usd',
                'payment_method' => $request->payment_method_id,
                'automatic_payment_methods' => [
                    'enabled' => true,
                    'allow_redirects' => 'never',
                ],
                'confirm' => true,
            ]);
    
            // Store the payment in the database
            Payment::create([
                'user_id' => auth()->id(),
                'membership_id' => $membership->id,
                'amount' => $amount_due,
                'status' => 'paid',
                'payment_method' => 'stripe',
                'transaction_reference' => $paymentIntent->id,
                'response_data' => json_encode($paymentIntent),
            ]);
    
            // Update membership data
            $membership->update([
                'payment_status' => 'paid',
                'payment_method' => 'stripe',
                'transaction_reference' => $paymentIntent->id,
                'amount_paid' => $membership->amount_paid + $amount_due // Add the amount to the already paid amount
            ]);
    
            return response()->json(['success' => true, 'message' => 'Payment successful!']);
        } catch (\Exception $e) {
            return response()->json(['success' => false, 'message' => 'Payment failed: ' . $e->getMessage()]);
        }
    }    

Screenshot: PaymentController file (PaymentController.php).

PaymentController file (PaymentController.php)

PaymentController file


5. Routing

Routes were added in routes/web.php:

Route::get('/payment/{membership_id}', [PaymentController::class, 'showPaymentForm'])->name('payment.form');
Route::post('/payment/process', [PaymentController::class, 'processPayment'])->name('payment.process');

Screenshot: Routes file (web.php).

Routes file (web.php)


6. Checkout Page

A checkout page (checkout.blade.php) was created for users to enter payment details.

<div class="container mt-4">
    <div class="col-md-6">

        <!-- Page title with increased size and bold font -->
        <h2 class="fw-bold mb-3" style="font-size: 28px;">Membership Payment</h2>

        <!-- Display membership pricing details -->
        <p><strong>Total Price:</strong> ${{ number_format($membership->price, 2) }}</p>
        <p><strong>Amount Paid:</strong> ${{ number_format($membership->amount_paid, 2) }}</p>

        @php
            // Calculate the remaining amount to be paid
            $amount_due = max($membership->price - $membership->amount_paid, 0);
        @endphp

        <p><strong>Amount Due:</strong> ${{ number_format($amount_due, 2) }}</p>

        <!-- Show payment form only if there is an outstanding amount -->
        @if($amount_due > 0)
            <form id="payment-form">
                <!-- Stripe Card Element -->
                <div id="card-element" class="form-control mb-3"></div>
                
                <!-- Payment button -->
                <button id="submit-button" class="btn btn-primary">
                    Pay ${{ number_format($amount_due, 2) }}
                </button>
            </form>
        @else
            <!-- Display a message when membership is fully paid -->
            <p class="text-success">Membership is fully paid!</p>
        @endif

        <!-- Display payment messages -->
        <p id="payment-message" class="text-danger mt-3 hidden"></p>
    </div>
</div>

Screenshot: Checkout page (checkout.blade.php).

Checkout page (checkout.blade.php)

Checkout page (checkout.blade.php)


7. Testing the Integration

Steps Performed:

  1. The Laravel server was started using php artisan serve.
  2. The checkout page was accessed via http://127.0.0.1:8000/checkout/{membership_id}.
  3. A test payment was submitted using a Stripe test token.

Screenshot: Memeberships table before payment in interface of regular user

Memeberships table before payment (regular user interface)

Screenshot: Checkout form before payment submission.

Checkout form before payment submission

Screenshot: Error when pressing the pay button without entering the card number

User can not pay without entering a card number

Screenshot: Error when pressing the pay button with an invalid card number entered

User can not pay with invalid card number

  1. After successful payment, the user was redirected to the success page.

Screenshot: Payment success page.

Payment success page

Screenshot: Payment success page with a pop-up window from the browser.

Payment success page with a pop-up window from the browser

  1. The database was checked to confirm that the transaction was recorded in the payments table.

Screenshot: 'Payments' table before first payment.

Empty payments table in database before payment

Screenshot: Database entry for the payment transaction.

Database entry for the payment transaction

Screenshot: Memeberships table after successfull payment in interface of regular user

Memeberships table after success payment in interface of regular user


8. Pushing the Project to GitHub

Steps Taken:

  • Committed changes:

git add .

git commit -m "Implemented Stripe payment integration with transaction logging and membership updates"

  • Checking if I am on a branch develop:

git branch

  • Pushed changes to GitHub:

git push origin develop

Successful push of main changes to GitHab


Conclusion

The Stripe payment gateway was successfully integrated into the Laravel project. Users can now pay for their memberships, and payment transactions are securely recorded in the database. The implementation ensures a structured approach by separating payment records into a dedicated payments table, making the system scalable and maintainable.



Webhook Handling (Optional)

Configuring webhooks is an essential step in receiving real-time payment status updates from Stripe. This ensures that the system stays synchronized with payment statuses, reflecting successful or failed transactions immediately.


1. Setting Up Stripe Webhook Listener

Before integrating the webhook, the Stripe CLI must be used to listen for webhook events and forward them to the local application.

stripe listen --forward-to http://127.0.0.1:8000/webhook/stripe

Screenshot: Stripe CLI log and webhook processing

Terminal showing the Stripe webhook listener active


2. Handling Webhook Events in Laravel

In Laravel, a WebhookController was created to handle incoming webhook requests from Stripe. The controller verifies the webhook signature and processes different types of events.

public function handle(Request $request)
{
    Stripe::setApiKey(env('STRIPE_SECRET'));

    // Retrieve the payload and signature from Stripe
    $payload = $request->getContent();
    $sigHeader = $request->header('Stripe-Signature');
    $endpointSecret = env('STRIPE_WEBHOOK_SECRET');

    try {
        $event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
    } catch (\Exception $e) {
        Log::error("Webhook signature verification failed: " . $e->getMessage());
        return response()->json(['error' => 'Invalid signature'], 400);
    }

    Log::info("Webhook received: " . $event->type);

    switch ($event->type) {
        case 'payment_intent.succeeded':
            $this->handleSuccessfulPayment($event->data->object);
            break;
        case 'payment_intent.payment_failed':
            $this->handleFailedPayment($event->data->object);
            break;
    }

    return response()->json(['status' => 'success'], 200);
}

Screenshot: Code editor displaying the handle method in WebhookController.php.

Code editor displaying the handle method in StripeWebhookController.php


3. Testing Webhook Events

Once the webhook is set up, events can be triggered using the Stripe CLI to ensure they are correctly received.

stripe trigger payment_intent.succeeded

If the webhook is correctly set up, the event should be logged in Laravel.

Screenshot: Terminal displaying webhook events received from Stripe.

Terminal displaying webhook events received from Stripe


4. Webhook Execution Results

After triggering a payment event, the system logs should confirm that the webhook was processed correctly. Additionally, the database should reflect the updated payment status.

  • Screenshot: Database view in phpMyAdmin showing the payments table with recorded transactions.

Database view in phpMyAdmin showing the payments table with recorded transactions

  • Screenshot: Admin panel displaying updated membership payment statuses.

Admin panel displaying updated membership payment statuses


This setup ensures that payments are automatically updated in real-time, improving system reliability and user experience.


5. Pushing the Project to GitHub

Steps Taken:

  • Committed changes:

git add .

git commit -m "Completed Stripe integration with real-time webhook handling for payment updates"

Screenshot: Git commit

Git commit

  • Switched to the develop branch:

git checkout develop

  • Pushed changes to GitHub:

git push origin develop

Screenshot: Git Push Output

Git Push Output


Conclusion

  • Successfully implemented email notifications for membership creation and expiration reminders.
  • Resolved timezone discrepancies between Laravel and MySQL.
  • Pushed the project to GitHub in the develop branch.
⚠️ **GitHub.com Fallback** ⚠️