Kali ini kita akan melanjutkan series tutorial yang paling penting yaitu integrasi Midtrans untuk metode pembayaran setelah melakukan checkout. Jika sebelumnya kita integrasi dengan API RajaOngkir, kali ini kita bermain dengan API Midtrans.
Langkah 1: Buat Akun di Midtrans
Buka Midtrans Dashboard dan daftar akun
Setelah login, buat Project Baru untuk aplikasi Anda.
Di dashboard pilih enviroment sandbox, kemudian di menu pengaturan, Anda akan mendapatkan Server Key dan Client Key. Simpan ini untuk konfigurasi Laravel nanti.
Langkah 2. Konfigurasi Midtrans
Install SDK Midtrans di Laravel:
composer require midtrans/midtrans-php
Konfigurasi API Key Midtrans: Tambahkan konfigurasi Midtrans di file .env
:
MIDTRANS_SERVER_KEY=your-server-key
MIDTRANS_CLIENT_KEY=your-client-key
MIDTRANS_IS_PRODUCTION=false
Buat file konfigurasi Midtrans di config/midtrans.php
:
<?php
return [
'serverKey' => env('MIDTRANS_SERVER_KEY'),
'clientKey' => env('MIDTRANS_CLIENT_KEY'),
'isProduction' => env('MIDTRANS_IS_PRODUCTION', false),
'is3ds' => true,
];
Tambahkan Service Provider di Laravel: Daftarkan konfigurasi di AppServiceProvider
:
use Midtrans\Config;
public function boot()
{
Config::$serverKey = config('midtrans.serverKey');
Config::$isProduction = config('midtrans.isProduction');
Config::$is3ds = config('midtrans.is3ds');
}
Langkah 3. Backend: Endpoint untuk Halaman Transaksi
Tambahkan metode di controller untuk memproses pembayaran dengan perintah:
php artisan make:controller OrderController
Edit file OrderController.php seperti berikut:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Midtrans\Snap;
use App\Models\Order; // Model untuk Order
use App\Models\OrderItem; // Model untuk item pesanan
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class OrderController extends Controller
{
public function createTransaction(Request $request)
{
DB::beginTransaction();
try {
// Data Order
$order = Order::create([
'user_id' => auth()->id(), // ID User
'total_price' => $request->totalPrice, // Total Price
'status' => 'pending', // Status awal
]);
// Simpan barang yang dipesan
foreach ($request->cartItems as $item) {
OrderItem::create([
'order_id' => $order->id,
'product_id' => $item['id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
]);
}
// Data untuk Midtrans
$transactionDetails = [
'order_id' => $order->id, // ID order unik
'gross_amount' => $request->totalPrice, // Total harga
];
$itemDetails = [];
foreach ($request->cartItems as $item) {
$itemDetails[] = [
'id' => $item['id'],
'price' => $item['price'],
'quantity' => $item['quantity'],
'name' => $item['name'],
];
}
// Tambahkan shipping cost sebagai item baru
$itemDetails[] = [
'id' => 'shipping_cost',
'price' => $request->shippingCost, // Ongkir
'quantity' => 1,
'name' => 'Shipping Cost',
];
$payload = [
'transaction_details' => $transactionDetails,
'item_details' => $itemDetails,
'customer_details' => [
'first_name' => auth()->user()->name,
'email' => auth()->user()->email,
],
];
// Buat URL pembayaran menggunakan Snap
$snapToken = Snap::getSnapToken($payload);
Log::info('Generated Snap Token: ' . $snapToken);
Log::info('Shipping Cost: ' . $request->shippingCost);
DB::commit();
return response()->json(['snapToken' => $snapToken, 'order_id' => $order->id,]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function handleNotification(Request $request)
{
$notification = new \Midtrans\Notification();
$order = Order::find($notification->id);
if (!$order) {
return response()->json(['error' => 'Order not found'], 404);
}
// Update status berdasarkan Midtrans
$transactionStatus = $notification->transaction_status;
if ($transactionStatus == 'capture' || $transactionStatus == 'settlement') {
$order->status = 'success';
} elseif ($transactionStatus == 'pending') {
$order->status = 'pending';
} else {
$order->status = 'failed';
}
$order->save();
return response()->json(['message' => 'Notification handled']);
}
public function updateStatus(Request $request)
{
$validated = $request->validate([
'id' => 'required|exists:orders,id',
'status' => 'required|string',
]);
$order = Order::findOrFail($validated['id']);
$order->update(['status' => $validated['status']]);
return response()->json(['message' => 'Order status updated successfully']);
}
}
Langkah 4. Vue.js: Memproses Pembayaran
Tambahkan fungsi untuk mengirim data ke backend dan memanggil Snap Popup Midtrans:
Buka file checkout.vue dan tambahkan method untuk memanggil OrderController:
async processPayment() {
try {
// Panggil backend untuk membuat order dan mendapatkan Snap Token
const response = await axios.post('/orders', {
cartItems: this.cartItems,
totalPrice: this.totalPrice + this.shippingCost,
shippingCost: this.shippingCost,
});
// Simpan data order ID dan Snap Token ke properti Vue
this.orderId = response.data.order_id; // Pastikan backend mengirimkan `id` order
this.snapToken = response.data.snapToken;
// Panggil Snap Midtrans
window.snap.pay(this.snapToken, {
onSuccess: async () => {
try {
// Akses `this.orderId` untuk mendapatkan ID order
alert(`Order ID: ${this.orderId}`);
// Perbarui status order di backend
await axios.post('/orders/update-status', {
id: this.orderId,
status: 'paid',
});
alert('Order status updated!');
} catch (error) {
console.error('Error updating order status:', error);
alert('Gagal memperbarui status order.');
}
},
onPending: () => {
alert('Payment sedang diproses!');
},
onError: () => {
alert('Payment gagal!');
},
});
} catch (error) {
console.error('Payment gagal:', error.response?.data || error.message);
alert('Terjadi kesalahan saat memproses pembayaran.');
}
},
di button bayar sekarang, panggil fungsi processPayment yang sudah kita defined tadi.
<!-- Tombol Lanjut ke Pembayaran -->
<button @click="processPayment" class="mt-6 w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md">
Bayar Sekarang
</button>
Kira kira source code lengkap untuk checkout.vue seperti berikut:
<template>
<div class="min-h-screen flex flex-col bg-gray-50">
<!-- NAVBAR -->
<header class="bg-white shadow z-10">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div class="text-2xl font-bold text-gray-800">MyShop</div>
<nav class="flex items-center space-x-6">
<Link href="/" class="text-gray-700 hover:text-gray-900 font-medium px-3 py-2 transition-colors">Home</Link>
</nav>
</div>
</header>
<main class="flex-1 py-10">
<div class="container mx-auto p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-6">Checkout</h1>
<!-- Error Message -->
<div v-if="error" class="bg-red-100 text-red-600 p-4 mb-6 rounded">
<p>{{ error }}</p>
</div>
<form @submit.prevent="submitOrder">
<!-- Address Information -->
<div class="mb-6">
<label for="address" class="block text-sm font-medium text-gray-700">Address</label>
<textarea id="address" v-model="address" class="mt-1 block w-full p-2 border rounded" required></textarea>
</div>
<!-- Select Province -->
<div class="mb-6">
<label for="province" class="block text-sm font-medium text-gray-700">Province</label>
<select v-model="selectedProvince" @change="fetchCities" class="mt-1 block w-full p-2 border rounded">
<option v-for="province in provinces" :value="province.id" :key="province.id">
{{ province.name }}
</option>
</select>
</div>
<!-- Select City -->
<div class="mb-6">
<label for="city" class="block text-sm font-medium text-gray-700">City</label>
<select id="destination" v-model="destination" class="mt-1 block w-full p-2 border rounded">
<option v-for="city in cities" :value="city.id" :key="city.id">
{{ city.name }}
</option>
</select>
</div>
<!-- Select Subdistrict -->
<div class="mb-6">
<label for="subdistrict" class="block text-sm font-medium text-gray-700">Subdistrict</label>
<select v-model="selectedSubdistrict" class="mt-1 block w-full p-2 border rounded" required>
<option value="" disabled selected>Select a subdistrict</option>
<option v-for="subdistrict in subdistricts" :key="subdistrict.subdistrict_id" :value="subdistrict.subdistrict_id">{{ subdistrict.subdistrict_name }}</option>
</select>
</div>
<!-- Shipping Cost Estimation -->
<div class="mb-6">
<label for="courier" class="block text-sm font-medium text-gray-700">Courier</label>
<select v-model="courier" @change="fetchShippingCost" class="mt-1 block w-full p-2 border rounded" required>
<option v-for="courier in couriers" :key="courier.code" :value="courier.code">
{{ courier.name }}
</option>
</select>
</div>
<!-- Select Services -->
<div class="mb-6">
<label for="city" class="block text-sm font-medium text-gray-700">Services</label>
<select id="shipping-service" v-model="selectedService" @change="fetchCost" class="mt-1 block w-full p-2 border rounded">
<option v-for="cost in shippingCosts" :key="cost.service" :value="cost">
{{ cost.service }}
{{ cost.cost[0].value | currency }} ({{ cost.cost[0].etd }} hari)
</option>
</select>
</div>
</form>
</div>
<!-- end kiri -->
<div>
<h2 class="text-lg font-semibold mb-4">Ringkasan Pesanan</h2>
<div class="bg-gray-100 p-4 rounded-md shadow-md">
<!-- Daftar Produk -->
<div v-for="item in cartItems" :key="item.id" class="flex justify-between mb-2">
<span>{{ item.name }}</span>
<span>Rp {{ formatCurrency(item.price) }} x {{ item.quantity }}</span>
</div>
<!-- Total Harga Barang -->
<div class="flex justify-between mt-4 border-t pt-2">
<span>Total Barang:</span>
<span>Rp {{ formatCurrency(totalPrice) }}</span>
</div>
<!-- Ongkos Kirim -->
<div class="flex justify-between mt-2">
<span>Ongkos Kirim:</span>
<span v-if="shippingCost">Rp {{ formatCurrency(shippingCost) }}</span>
<span v-else>Rp 0</span>
</div>
<!-- Total Harga -->
<div class="flex justify-between mt-4 border-t pt-2 font-semibold text-lg">
<span>Total Bayar:</span>
<span>Rp {{ formatCurrency(totalPrice + shippingCost) }}</span>
</div>
</div>
<!-- Tombol Lanjut ke Pembayaran -->
<button @click="processPayment" class="mt-6 w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md">
Bayar Sekarang
</button>
</div>
</div>
</div>
</main>
<footer class="bg-white py-4">
<div class="max-w-7xl mx-auto px-4 text-center text-gray-600 text-sm">
© 2025 MyShop. All rights reserved.
</div>
</footer>
</div>
</template>
<script>
import {ref, computed } from 'vue';
import axios from 'axios';
export default {
data() {
return {
provinces: [],
cities: [],
selectedProvince: null,
selectedCity: null,
origin: '501', // ID kota asal (contoh: Semarang)
destination: null, // ID kota tujuan (dipilih user)
weight: 1000, // Berat barang dalam gram
courier: null, // Kurir (dipilih user)
shippingCosts: [], // Menyimpan data ongkir
couriers: [
{ name: 'JNE', code: 'jne' },
{ name: 'POS', code: 'pos' },
{ name: 'TIKI', code: 'tiki' },
],
selectedService: null, // Layanan pengiriman yang dipilih
shippingCost: 0, // Biaya pengiriman
orderId: null, // ID order, diisi setelah membuat order
snapToken: null, // Token Snap Midtrans, diisi setelah membuat order
};
},
setup() {
// Retrieve the cart from localStorage, or initialize an empty cart
const cartItems = ref(JSON.parse(localStorage.getItem('cart')) || []);
// Calculate total price
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => total + item.subTotal, 0);
});
const totalwithShipping = computed(()=>{
return this.totalPrice + this.shippingCost;
})
// Initialize subTotal for each item
cartItems.value.forEach(item => {
item.subTotal = item.price * item.quantity;
});
return {
cartItems,
totalPrice,
};
},
methods: {
formatCurrency(amount) {
const formatted = amount.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
// Hapus desimal jika hanya berisi .00
return formatted.replace(/,00$/, "");
},
async fetchProvinces() {
const response = await axios.get('/provinces');
this.provinces = response.data;
},
async fetchCities() {
const response = await axios.get(`/cities/${this.selectedProvince}`);
this.cities = response.data;
},
async fetchCost() {
console.log('Selected service:', this.selectedService);
const selected = this.selectedService; // Karena sekarang selectedService sudah berupa objek
if (selected) {
this.shippingCost = selected.cost[0].value;
} else {
console.error('No matching service found');
}
},
async fetchShippingCost() {
if (!this.destination || !this.courier) {
alert('Pilih kota tujuan dan kurir!');
return;
}
try {
const response = await axios.post('/shipping/cost', {
origin: this.origin,
destination: this.destination,
weight: this.weight,
courier: this.courier,
});
this.shippingCosts = response.data[0].costs; // Ambil biaya pengiriman
} catch (error) {
console.error('Failed to fetch shipping cost:', error);
}
},
async processPayment() {
try {
// Panggil backend untuk membuat order dan mendapatkan Snap Token
const response = await axios.post('/orders', {
cartItems: this.cartItems,
totalPrice: this.totalPrice + this.shippingCost,
shippingCost: this.shippingCost,
});
// Simpan data order ID dan Snap Token ke properti Vue
this.orderId = response.data.order_id; // Pastikan backend mengirimkan `id` order
this.snapToken = response.data.snapToken;
// Panggil Snap Midtrans
window.snap.pay(this.snapToken, {
onSuccess: async () => {
try {
// Akses `this.orderId` untuk mendapatkan ID order
alert(`Order ID: ${this.orderId}`);
// Perbarui status order di backend
await axios.post('/orders/update-status', {
id: this.orderId,
status: 'paid',
});
alert('Order status updated!');
} catch (error) {
console.error('Error updating order status:', error);
alert('Gagal memperbarui status order.');
}
},
onPending: () => {
alert('Payment sedang diproses!');
},
onError: () => {
alert('Payment gagal!');
},
});
} catch (error) {
console.error('Payment gagal:', error.response?.data || error.message);
alert('Terjadi kesalahan saat memproses pembayaran.');
}
},
},
mounted() {
this.fetchProvinces();
const script = document.createElement('script');
script.src = 'https://app.sandbox.midtrans.com/snap/snap.js';
script.type = 'text/javascript';
script.setAttribute('data-client-key', 'SB-Mid-client-YR7We1Ycxm1E-kzs');
document.body.appendChild(script);
},
};
</script>
<style scoped>
/* Styling */
</style>
Langkah 5. Update Model dan Database
Migration
Buat migrasi untuk orders
dan order_items
:
php artisan make:migration create_orders_table
php artisan make:migration create_order_items_table
Edit Migration Orders Table:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->decimal('total_price', 15, 2);
$table->string('status')->default('pending');
$table->timestamps();
});
Migration Order Items Table:
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->onDelete('cascade');
$table->foreignId('product_id')->constrained()->onDelete('cascade');
$table->integer('quantity');
$table->decimal('price', 15, 2);
$table->timestamps();
});
Lalu ketik perintah migrate:
php artisan migrate
Buat Order Model dengan perintah berikut:
php artisan make:model Order
Edit Order Model menjadi berikut:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected $fillable = ['user_id', 'total_price', 'status'];
public function items()
{
return $this->hasMany(OrderItem::class);
}
}
Kemudian buat model OrderItem untuk menyimpan item-item yang diorder.
php artisan make:model OrderItem
Lalu isi OrderItem sebagai berikut:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class OrderItem extends Model
{
protected $fillable = ['order_id', 'product_id', 'quantity', 'price'];
public function order()
{
return $this->belongsTo(Order::class);
}
}
Langkah 6. Tambahkan Routes
Tambahkan di file routes/web.php
:
use App\Http\Controllers\OrderController;
Route::post('/orders', [OrderController::class, 'createTransaction'])->name('orders');
Route::post('/orders/update-status', [OrderController::class, 'updateStatus']);
// Endpoint untuk menerima notifikasi dari Midtrans
Route::post('/orders/notification', [OrderController::class, 'handleNotification'])->name('orders.notification');
Langkah 7. Testing
Jika semua sudah benar, akan menghasilkan seperti berikut:
Klik Bayar sekarang, maka akan muncul snap midtrans seperti berikut:
Lalu lakukan simulasi pembayaran di Simulator Sandbox Midtrans:
Jika sudah berhasil, maka otomatis payment di frontend akan berhasil, dan ada notif seperti berikut:
Dan otomatis data di database, status nya berubah jadi paid.
Selamat Mencoba!!!!