Berikut adalah langkah-langkah membuat halaman checkout dengan menghitung biaya ongkir dengan menggunakan Api RajaOngkir.
Untuk database province, city dan subdistrict kita akan simpan di database lokal agar tidak terlalu banyak request ke API.
Langkah 1: Daftar dan Dapatkan API Key RajaOngkir
Daftar akun di RajaOngkir.
Pilih paket yang sesuai (Starter sudah cukup untuk testing).
Salin API Key yang diberikan di dashboard RajaOngkir.
Buka file .env, kemudian buat variable untuk isi API key yang didapat sebelumnya seperti berikut:
RAJAONGKIR_API_KEY=api_key_rajaongkir
Langkah 2: Migrasi dan Seeder untuk Data Provinsi dan City
Buat tabel untuk menyimpan data provinsi, kabupaten/kota, dan kecamatan.
Buat Migration
Buat migration untuk tabel provinces
, cities
, dan subdistricts
.
php artisan make:migration create_provinces_table
php artisan make:migration create_cities_table
php artisan make:migration create_subdistricts_table
Isi file migrasi:
create_provinces_table
Schema::create('provinces', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
create_cities_table
Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->foreignId('province_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->timestamps();
});
create_subdistricts_table
Schema::create('subdistricts', function (Blueprint $table) {
$table->id();
$table->foreignId('city_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->timestamps();
});
Buat model Province dan City dengan menggunakan perintah berikut:
php artisan make:model Province
php artisan make:model City
Untuk subsdistrict kita tidak akan buat, karena hanya Rajaongkir versi Pro yang bisa akses. Jadi kita sebagai latihan cukup provinsi dan kabupaten atau city.
Lalu jalankan Migrasi
php artisan migrate
Seeder untuk Data Lokasi
Gunakan API RajaOngkir sekali untuk mengambil data provinsi, kota, dan kecamatan, lalu simpan ke database.
Command untuk Fetch Data
Buat command untuk mengambil data dari API RajaOngkir dan menyimpannya ke database.
php artisan make:command FetchRajaOngkirData
Buka file command di app/Console/Command dan Isi file command:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Province;
use App\Models\City;
use Illuminate\Support\Facades\Http;
class FetchRajaOngkirData extends Command
{
protected $signature = 'fetch:rajaongkir';
protected $description = 'Fetch provinces, cities, and subdistricts from RajaOngkir API';
public function handle()
{
// Fetch provinces
$response = Http::withHeaders(['key' => env('RAJAONGKIR_API_KEY')])
->get('https://api.rajaongkir.com/starter/province');
$provinces = $response->json()['rajaongkir']['results'];
foreach ($provinces as $province) {
Province::updateOrCreate(['id' => $province['province_id']], ['name' => $province['province']]);
}
// Fetch cities
$response = Http::withHeaders(['key' => env('RAJAONGKIR_API_KEY')])
->get('https://api.rajaongkir.com/starter/city');
$cities = $response->json()['rajaongkir']['results'];
foreach ($cities as $city) {
City::updateOrCreate(
['id' => $city['city_id']],
[
'province_id' => $city['province_id'],
'name' => $city['city_name']
]
);
}
$this->info('Provinces and cities have been imported successfully!');
}
}
Lalu Jalankan perintah:
php artisan fetch:rajaongkir
Jika langkah ini berhasil, maka akan terdapat data province dan kabupaten yang sudah dilakukan fetch melalui API rajaongkir dan disimpan ke database local yang sudah kita buat sebelumnya seperi berikut:
Tabel Province
Tabel Cities
Langkah 3: Menampilkan data di Frontend
Buat file Controller checkout untuk menghandle halaman checkout ini dengan perintah:
php artisan make:controller CheckoutController
Buka file CheckoutController dan tambahkan function berikut:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Inertia\Inertia;
use App\Models\Province;
use App\Models\City;
class CheckoutController extends Controller
{
public function __construct()
{
//$this->middleware('auth'); // Pastikan user login untuk mengakses checkout
}
// Menampilkan halaman checkout dengan data provinsi
public function index()
{
$provinces = $this->getProvinces();
return Inertia::render('Frontend/Checkout', [
'provinces' => $provinces
]);
}
// Mendapatkan data provinsi dari API RajaOngkir
public function getProvinces()
{
$provinces = Province::all();
return response()->json($provinces);
}
// Mendapatkan data kota berdasarkan provinsi
public function getCities($province_id)
{
$cities = City::where('province_id', $province_id)->get();
return response()->json($cities);
}
// Mendapatkan data subdistrict berdasarkan kota
public function getSubdistricts(Request $request)
{
$cityId = $request->city_id;
$response = Http::get('https://api.rajaongkir.com/starter/subdistricts', [
'key' => env('RAJAONGKIR_API_KEY'),
'city' => $cityId,
]);
if ($response->successful()) {
return response()->json($response->json()['rajaongkir']['results']);
}
return response()->json([]);
}
// Estimasi ongkos kirim
public function getShippingCost(Request $request)
{
$apiKey = env('RAJAONGKIR_API_KEY'); // API Key Anda
$url = 'https://api.rajaongkir.com/starter/cost';
$response = Http::withHeaders([
'key' => $apiKey,
])->post($url, [
'origin' => $request->origin, // ID kota asal
'destination' => $request->destination, // ID kota tujuan
'weight' => $request->weight, // Berat dalam gram
'courier' => $request->courier, // Kurir: jne, pos, tiki
]);
if ($response->successful()) {
return response()->json($response->json()['rajaongkir']['results']);
}
return response()->json(['error' => 'Failed to fetch shipping cost'], 500);
}
}
Dari controller di atas, kita sudah buat fungsi untuk getProvince, getCity dan getShippingcost berbentuk json yang akan kita kirimkan ke frontend.
Kemudian buat file Checkout.vue di folder resources/js/Pages/ dan isi sebagai 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="continueToPayment" class="mt-6 w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md">
Continue to Payment
</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';
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
};
},
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);
}
},
},
mounted() {
this.fetchProvinces();
},
};
</script>
<style scoped>
/* Styling */
</style>
Langkah 4: Tambahkan route
Buka file web.php di folder routes, dan tambahkan definisi route berikut:
use App\Http\Controllers\CheckOutController;
Route::get('/provinces', [CheckoutController::class, 'getProvinces'])->name('provinces');
Route::post('/checkout/cities', [CheckoutController::class, 'getCities']);
Route::post('/checkout/subdistricts', [CheckoutController::class, 'getSubdistricts']);
Route::post('/checkout/shipping-cost', [CheckoutController::class, 'getShippingCost']);
Check hasil:
Jika langkah di atas sudah diikuti dengan benar, maka akan menghasilkan result berikut: