Integrating Paytmjs Checkout In Laravel And Vuejs

image
image
image
image
image
image
image
image
Integrating PaytmJS Checkout in Laravel and Vue.js

Integrating PaytmJS Checkout in Laravel and Vue.js

In this comprehensive guide, we'll walk through the process of integrating PaytmJS Checkout into a Laravel backend with a Vue.js frontend. This integration will enable your application to accept payments securely through the Paytm payment gateway.


Prerequisites

Before we begin, ensure you have the following:

  1. Laravel project (8.x or higher recommended)
  2. Vue.js set up within Laravel or as a separate application
  3. Paytm merchant account with:
  4. Merchant ID
  5. Merchant Key
  6. Website name
  7. Industry type
  8. Composer installed
  9. npm or yarn installed


Step 1: Install Paytm Payment Package

First, we need to install a Laravel package to handle Paytm payment gateway integration. Let's use the popular anandsiddharth/laravel-paytm-wallet package:

composer require anandsiddharth/laravel-paytm-wallet


Step 2: Publish Package Configuration

Publish the configuration file:

php artisan vendor:publish --provider="Anand\LaravelPaytmWallet\PaytmWalletServiceProvider"


Step 3: Configure Paytm Credentials

Open the configuration file at config/paytm.php and update it with your Paytm credentials:

<?php

return [
'environment' => env('PAYTM_ENVIRONMENT', 'production'), // values: 'production', 'local'
'merchant_id' => env('PAYTM_MERCHANT_ID', ''),
'merchant_key' => env('PAYTM_MERCHANT_KEY', ''),
'merchant_website' => env('PAYTM_WEBSITE', ''),
'channel' => env('PAYTM_CHANNEL', 'WEB'),
'industry_type' => env('PAYTM_INDUSTRY_TYPE', 'Retail'),
];


Step 4: Add Environment Variables

Update your .env file with your Paytm credentials:

PAYTM_ENVIRONMENT=local
PAYTM_MERCHANT_ID=YOUR_MERCHANT_ID
PAYTM_MERCHANT_KEY=YOUR_MERCHANT_KEY
PAYTM_WEBSITE=WEBSTAGING
PAYTM_CHANNEL=WEB
PAYTM_INDUSTRY_TYPE=Retail


Creating the Payment API

Step 1: Create Payment Model and Migration

Generate a payment model and migration:

php artisan make:model Payment -m

Update the migration file:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePaymentsTable extends Migration
{
public function up()
{
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique();
$table->string('user_id')->nullable();
$table->decimal('amount', 10, 2);
$table->string('transaction_id')->nullable();
$table->string('status')->default('pending');
$table->json('payment_data')->nullable();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('payments');
}
}
Run the migration:
php artisan migrate


Step 2: Create Payment Controller

Generate a payment controller:

php artisan make:controller PaymentController


Update the controller with the following code:

<?php

namespace App\Http\Controllers;

use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Anand\LaravelPaytmWallet\Facades\PaytmWallet;

class PaymentController extends Controller
{
/**
* Initiate a payment
*/
public function initiate(Request $request)
{
$request->validate([
'amount' => 'required|numeric|min:1',
'user_id' => 'required',
]);

// Generate a unique order ID
$orderId = 'ORDER' . time() . Str::random(6);
// Store payment info in database
$payment = Payment::create([
'order_id' => $orderId,
'user_id' => $request->user_id,
'amount' => $request->amount,
'status' => 'pending',
]);

// Get payment initiation data
$paymentData = PaytmWallet::with('receive')
->setOrderId($orderId)
->setAmount($request->amount)
->setCustomerId($request->user_id)
->getTransactionToken();

// Return checkout data for frontend
return response()->json([
'order_id' => $orderId,
'amount' => $request->amount,
'token' => $paymentData['body']['txnToken'],
'merchant_id' => config('paytm.merchant_id'),
'callback_url' => route('paytm.callback'),
]);
}

/**
* Handle payment callback
*/
public function callback(Request $request)
{
$transaction = PaytmWallet::with('receive');
$response = $transaction->response();
$order_id = $transaction->getOrderId();
// Get payment record
$payment = Payment::where('order_id', $order_id)->first();
if($transaction->isSuccessful()) {
// Payment successful
$payment->update([
'status' => 'completed',
'transaction_id' => $response['TXNID'],
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.success')->with('success', 'Payment successful');
} else if($transaction->isFailed()) {
// Payment failed
$payment->update([
'status' => 'failed',
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.failed')->with('error', 'Payment failed');
} else if($transaction->isOpen()) {
// Payment pending
$payment->update([
'status' => 'pending',
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.pending')->with('info', 'Payment pending');
}
// Fallback
return redirect()->route('payment.failed')->with('error', 'Something went wrong');
}

/**
* Check payment status
*/
public function status($orderId)
{
$payment = Payment::where('order_id', $orderId)->first();
if(!$payment) {
return response()->json(['error' => 'Payment not found'], 404);
}
return response()->json([
'status' => $payment->status,
'order_id' => $payment->order_id,
'amount' => $payment->amount,
'transaction_id' => $payment->transaction_id,
]);
}
}


Step 3: Define Routes

Add the following routes to your routes file (routes/api.php):

<?php

use App\Http\Controllers\PaymentController;

// Payment routes
Route::post('/payment/initiate', [PaymentController::class, 'initiate']);
Route::post('/payment/callback', [PaymentController::class, 'callback'])->name('paytm.callback');
Route::get('/payment/status/{orderId}', [PaymentController::class, 'status']);

// Result pages
Route::view('/payment/success', 'payment.success')->name('payment.success');
Route::view('/payment/failed', 'payment.failed')->name('payment.failed');
Route::view('/payment/pending', 'payment.pending')->name('payment.pending');


Implementing Vue.js Frontend

Step 1: Create Payment Component

Create a new Vue component for handling Paytm payments:

# If using Laravel Mix
touch resources/js/components/PaytmCheckout.vue

Add the following code to the component:

<template>
<div class="paytm-checkout">
<h2>Payment Details</h2>
<div class="form-group">
<label for="amount">Amount</label>
<input
type="number"
id="amount"
v-model="amount"
class="form-control"
:disabled="loading"
>
</div>
<button
@click="initiatePayment"
class="btn btn-primary"
:disabled="loading || !amount"
>
{{ loading ? 'Processing...' : 'Pay Now' }}
</button>
<div v-if="error" class="alert alert-danger mt-3">
{{ error }}
</div>
</div>
</template>

<script>
export default {
data() {
return {
amount: 100,
userId: 'USER123', // This should be dynamically set from your auth system
loading: false,
error: null,
checkoutConfig: null
};
},
methods: {
async initiatePayment() {
this.loading = true;
this.error = null;
try {
// Step 1: Get payment token from backend
const response = await axios.post('/api/payment/initiate', {
amount: this.amount,
user_id: this.userId
});
// Step 2: Store payment config
this.checkoutConfig = {
orderId: response.data.order_id,
token: response.data.token,
amount: response.data.amount,
merchantId: response.data.merchant_id,
callbackUrl: response.data.callback_url
};
// Step 3: Load Paytm JS
await this.loadPaytmScript();
// Step 4: Open checkout
this.openPaytmCheckout();
} catch (error) {
console.error('Payment initiation failed:', error);
this.error = error.response?.data?.message || 'Failed to initiate payment. Please try again.';
} finally {
this.loading = false;
}
},
loadPaytmScript() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://securegw.paytm.in/merchantpgpui/checkoutjs/merchants/' + this.checkoutConfig.merchantId + '.js';
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
},
openPaytmCheckout() {
if (typeof window.Paytm === 'undefined') {
this.error = 'Failed to load Paytm checkout. Please try again.';
return;
}
// Configure checkout
const config = {
root: "",
flow: "DEFAULT",
data: {
orderId: this.checkoutConfig.orderId,
token: this.checkoutConfig.token,
tokenType: "TXN_TOKEN",
amount: this.checkoutConfig.amount
},
merchant: {
mid: this.checkoutConfig.merchantId,
redirect: true
},
handler: {
notifyMerchant: (eventName, data) => {
console.log("notifyMerchant handler function called");
console.log("eventName => ", eventName);
console.log("data => ", data);
if (eventName === 'APP_CLOSED') {
// User closed the payment page
this.checkPaymentStatus();
}
}
}
};
// Initialize and invoke Paytm checkout
window.Paytm.CheckoutJS.init(config)
.then(() => {
window.Paytm.CheckoutJS.invoke();
})
.catch(error => {
console.error("Error in CheckoutJS init", error);
this.error = 'Failed to initialize payment gateway. Please try again.';
});
},
async checkPaymentStatus() {
if (!this.checkoutConfig?.orderId) return;
try {
const statusResponse = await axios.get(`/api/payment/status/${this.checkoutConfig.orderId}`);
const status = statusResponse.data.status;
if (status === 'completed') {
alert('Payment successful!');
// Redirect or update UI as needed
} else if (status === 'failed') {
this.error = 'Payment failed. Please try again.';
} else if (status === 'pending') {
this.error = 'Payment is pending. We will update you once confirmed.';
}
} catch (error) {
console.error('Failed to check payment status:', error);
}
}
}
};
</script>

<style scoped>
.paytm-checkout {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}

.form-group {
margin-bottom: 15px;
}

.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}

.btn-primary {
background-color: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}

.btn-primary:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>


Step 2: Register Component in Your App

Import and register the component in your main Vue application file:

// resources/js/app.js
import { createApp } from 'vue';
import PaytmCheckout from './components/PaytmCheckout.vue';

const app = createApp({});

app.component('paytm-checkout', PaytmCheckout);
app.mount('#app');


Step 3: Add Component to Your View

Add the component to a Laravel Blade view:

<!-- resources/views/checkout.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div id="app">
<paytm-checkout></paytm-checkout>
</div>
</div>
</div>
</div>
@endsection
Add route for the checkout page:
// routes/web.php
Route::get('/checkout', function () {
return view('checkout');
});


Handling Payment Callbacks

When a payment is completed, Paytm will redirect to the callback URL we provided. This callback is handled by the callback method in our PaymentController.

Let's create simple views for payment results:

mkdir -p resources/views/payment
touch resources/views/payment/success.blade.php
touch resources/views/payment/failed.blade.php
touch resources/views/payment/pending.blade.php


Success Page

<!-- resources/views/payment/success.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
<div class="alert alert-success">
<h2>Payment Successful!</h2>
<p>Your transaction has been completed successfully.</p>
<a href="/" class="btn btn-primary">Return to Home</a>
</div>
</div>
@endsection


Failed Page

<!-- resources/views/payment/failed.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
<div class="alert alert-danger">
<h2>Payment Failed</h2>
<p>Your transaction could not be completed. Please try again.</p>
<a href="/checkout" class="btn btn-primary">Try Again</a>
</div>
</div>
@endsection


Pending Page

<!-- resources/views/payment/pending.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
<div class="alert alert-warning">
<h2>Payment Pending</h2>
<p>Your transaction is being processed. We will notify you once it's complete.</p>
<a href="/" class="btn btn-primary">Return to Home</a>
</div>
</div>
@endsection


Testing the Integration

To test your Paytm integration:

  1. Make sure your application is running
  2. Visit the checkout page at /checkout
  3. Enter an amount and click "Pay Now"
  4. You'll be redirected to the Paytm payment page
  5. For testing, use Paytm's test credentials:
  6. Mobile Number: 7777777777
  7. OTP: Any 6 digits
  8. Debit Card: Any valid card format with CVV 123
  9. Complete the payment
  10. You'll be redirected back to your application


Best Practices and Security Considerations

  1. Environment Variables: Always store sensitive credentials in environment variables, never hardcode them.
  2. Data Validation: Validate all inputs, especially amount and user data.
  3. HTTPS: Use HTTPS for all payment-related routes.
  4. Testing Mode: Use Paytm's test environment before going live.
  5. Error Handling: Implement comprehensive error handling for payment failures.
  6. Idempotency: Ensure that the same payment can't be processed twice.
  7. Logging: Implement logging for payment activities for audit purposes.
  8. Webhooks: Consider implementing additional webhook support for asynchronous payment updates.


Common Issues:

Callback URL Issues:

  1. Make sure your callback URL is registered in your Paytm merchant dashboard
  2. For local testing, you may need to use a service like ngrok

CSRF Token Mismatch:

  1. Add an exception for the callback route in your VerifyCsrfToken middleware:
protected $except = [
'payment/callback',
];

Transaction Token Error:

  1. Verify your merchant ID and key
  2. Check that you're using the correct environment (test/production)

Payment Status Not Updating:

  1. Check your database connection
  2. Verify that the order ID is being properly tracked

Paytm JS Not Loading:

  1. Check browser console for errors
  2. Verify merchant ID in the JS URL


Debug Tips:

  1. Check the browser console for JavaScript errors
  2. Monitor Laravel logs for backend errors
  3. Use Paytm's dashboard to track transaction status
  4. For detailed debugging, implement more extensive logging in your payment controller


Conclusion

You've now successfully integrated PaytmJS Checkout into your Laravel and Vue.js application. This setup provides a secure and seamless payment experience for your users.

Remember to thoroughly test the integration in Paytm's testing environment before moving to production. Also, make sure to keep your Paytm credentials secure and follow best practices for handling sensitive payment information.