TASKS 17: Payment Gateway Integration - RadLeoOFC/laravel-admin-panel GitHub Wiki
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:
The Stripe PHP SDK was installed using Composer. The following command was executed:
composer require stripe/stripe-phpScreenshot: Successful installation of Stripe SDK.

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.

The migration was executed using:
php artisan migrateScreenshot: Successful execution of the migration.

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).
.jpg?raw=true)
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).
.jpg?raw=true)

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).
..jpg?raw=true)
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).
.jpg?raw=true)
..jpg?raw=true)
- The Laravel server was started using
php artisan serve. - The checkout page was accessed via
http://127.0.0.1:8000/checkout/{membership_id}. - A test payment was submitted using a Stripe test token.
Screenshot: Memeberships table before payment in interface of regular user
.jpg?raw=true)
Screenshot: Checkout form before payment submission.

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

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

- After successful payment, the user was redirected to the success page.
Screenshot: Payment success page.

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

- The database was checked to confirm that the transaction was recorded in the
paymentstable.
Screenshot: 'Payments' table before first payment.

Screenshot: Database entry for the payment transaction.

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

- 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

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.
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.
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/stripeScreenshot: Stripe CLI log and webhook processing

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.

Once the webhook is set up, events can be triggered using the Stripe CLI to ensure they are correctly received.
stripe trigger payment_intent.succeededIf the webhook is correctly set up, the event should be logged in Laravel.
Screenshot: Terminal displaying webhook events received from Stripe.

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
paymentstable with recorded transactions.

- Screenshot: Admin panel displaying updated membership payment statuses.

This setup ensures that payments are automatically updated in real-time, improving system reliability and user experience.
- Committed changes:
git add .
git commit -m "Completed Stripe integration with real-time webhook handling for payment updates"
Screenshot: Git commit

- Switched to the
developbranch:
git checkout develop
- Pushed changes to GitHub:
git push origin develop
Screenshot: Git Push Output

- Successfully implemented email notifications for membership creation and expiration reminders.
- Resolved timezone discrepancies between Laravel and MySQL.
- Pushed the project to GitHub in the
developbranch.