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

Province fetch API

Tabel Cities

cities tabel

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">
          &copy; 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:
Checkout Rajaongkir