aliziodev / laravel-biteship
Laravel package for Biteship shipping aggregator API integration
Requires
- php: ^8.3
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
README
Package Laravel untuk integrasi API Biteship - aggregator pengiriman. Mempermudah perhitungan ongkir, pembuatan order, pelacakan, dan penanganan webhook di aplikasi Laravel Anda.
Fitur
- ✅ Rates API - Cek ongkir dengan caching cerdas
- ✅ Orders API - Buat, ambil, dan batalkan order
- ✅ Tracking API - Lacak pengiriman berdasarkan order ID atau resi
- ✅ Couriers API - Daftar kurir yang tersedia
- ✅ Locations API - Kelola lokasi tersimpan
- ✅ Webhook Handler - Terima dan dispatch event Laravel untuk webhook Biteship
- ✅ Label Generator - Generate label pengiriman dari data order
- ✅ Rate Caching - Caching cerdas untuk mengurangi panggilan API
- ✅ Type Safety - Type hints lengkap dengan DTOs dan Enums
- ✅ Exception Handling - Exception terstruktur untuk error API
Instalasi
Install package via Composer:
composer require aliziodev/laravel-biteship
Jalankan perintah install untuk publish konfigurasi dan migrasi opsional:
php artisan biteship:install
Perintah install akan:
- Publish file konfigurasi ke
config/biteship.php - Opsional publish migrasi database (jika Anda memilih menggunakan layer DB)
- Cek apakah
BITESHIP_API_KEYsudah di-set di.env
Konfigurasi
Tambahkan API key Biteship Anda ke file .env:
BITESHIP_API_KEY=biteship_live.your_api_key_here
Untuk mode sandbox/testing, gunakan test key:
BITESHIP_API_KEY=biteship_test.your_test_key_here
Konfigurasi Lengkap
File config/biteship.php diorganisir dalam 7 bagian. Berikut penjelasan dan contoh penggunaannya:
return [ /* * 1. KONEKSI & AUTENTIKASI * ========================= * Wajib diisi. Prefix key menentukan mode: * - biteship_test.* → sandbox * - biteship_live.* → production */ 'api_key' => env('BITESHIP_API_KEY'), 'base_url' => env('BITESHIP_BASE_URL', 'https://api.biteship.com'), 'timeout' => env('BITESHIP_TIMEOUT', 30), /* * 2. WEBHOOK * ========================= * Konfigurasi endpoint untuk menerima notifikasi dari Biteship. * Signature verification sangat disarankan untuk production. */ 'webhook' => [ 'path' => env('BITESHIP_WEBHOOK_PATH', 'biteship/webhook'), 'middleware' => ['api'], 'signature_key' => env('BITESHIP_WEBHOOK_SIGNATURE_KEY'), 'signature_secret' => env('BITESHIP_WEBHOOK_SIGNATURE_SECRET'), ], /* * 3. ORIGIN PENGIRIMAN (DEFAULT) * ========================= * Lokasi pickup default — gudang, toko, atau titik pengambilan paket. * Wajib set salah satu: area_id ATAU postal_code. */ 'default_origin' => [ 'area_id' => env('BITESHIP_ORIGIN_AREA_ID'), 'postal_code' => env('BITESHIP_ORIGIN_POSTAL_CODE'), 'contact_name' => env('BITESHIP_ORIGIN_CONTACT_NAME'), 'contact_phone' => env('BITESHIP_ORIGIN_CONTACT_PHONE'), 'contact_email' => env('BITESHIP_ORIGIN_CONTACT_EMAIL'), 'address' => env('BITESHIP_ORIGIN_ADDRESS'), 'note' => env('BITESHIP_ORIGIN_NOTE'), ], /* * 4. CACHE RATES * ========================= * Hindari rate limit dengan cache. TTL default 15 menit. */ 'cache' => [ 'enabled' => env('BITESHIP_CACHE_ENABLED', true), 'ttl' => env('BITESHIP_CACHE_TTL', 900), 'store' => env('BITESHIP_CACHE_STORE', null), 'prefix' => env('BITESHIP_CACHE_PREFIX', 'biteship'), ], /* * 5. MOCK MODE * ========================= * Development & testing tanpa hit API asli. */ 'mock_mode' => [ 'enabled' => env('BITESHIP_MOCK_MODE', false), 'validation' => env('BITESHIP_MOCK_VALIDATION', true), 'delay' => env('BITESHIP_MOCK_DELAY', 0), 'errors' => [ 'authentication' => env('BITESHIP_MOCK_ERROR_AUTH', false), 'rate_limit' => env('BITESHIP_MOCK_ERROR_RATE_LIMIT', false), 'validation' => env('BITESHIP_MOCK_ERROR_VALIDATION', false), 'server' => env('BITESHIP_MOCK_ERROR_SERVER', false), ], ], /* * 6. LABEL * ========================= * View untuk generate label cetak. */ 'label' => [ 'view' => env('BITESHIP_LABEL_VIEW', 'biteship::label'), ], /* * 7. SHIPPER DEFAULT * ========================= * Branding pengirim di label — nama toko, kontak, dll. * Optional, terpisah dari origin. */ 'default_shipper' => [ 'contact_name' => env('BITESHIP_SHIPPER_CONTACT_NAME'), 'contact_phone' => env('BITESHIP_SHIPPER_CONTACT_PHONE'), 'contact_email' => env('BITESHIP_SHIPPER_CONTACT_EMAIL'), 'organization' => env('BITESHIP_SHIPPER_ORGANIZATION'), ], ];
Mock Mode (Development & Testing)
Package mendukung Mock Mode untuk development dan testing tanpa perlu hit API Biteship asli. Mock Mode hanya tersedia untuk Rates API dan Orders API.
Aktifkan Mock Mode
BITESHIP_MOCK_MODE=true
Konfigurasi Mock Mode
# Aktifkan mock mode BITESHIP_MOCK_MODE=true # Validasi input (seperti real API) BITESHIP_MOCK_VALIDATION=true # Artificial delay (dalam ms) BITESHIP_MOCK_DELAY=500 # Error simulation (untuk testing error handling) BITESHIP_MOCK_ERROR_AUTH=false # Simulasi 401 BITESHIP_MOCK_ERROR_RATE_LIMIT=false # Simulasi 429 BITESHIP_MOCK_ERROR_VALIDATION=false # Simulasi 422 BITESHIP_MOCK_ERROR_SERVER=false # Simulasi 500
Contoh Penggunaan Mock Mode
use Aliziodev\Biteship\Facades\Biteship; use Aliziodev\Biteship\DTOs\Rate\RateRequest; // Mock mode diaktifkan - tidak perlu API key! $request = (new RateRequest) ->originAreaId('IDNP10001') ->originContact('Budi', '08123456789') ->originAddress('Jl. Sudirman No.1') ->destinationAreaId('IDNP20001') ->destinationContact('Ani', '08987654321') ->destinationAddress('Jl. Merdeka No.10') ->addItem(['name' => 'Baju', 'value' => 100000, 'weight' => 500, 'quantity' => 1]); $response = Biteship::rates()->check($request); // Response dynamic dengan random ID dan harga // Courier: JNE, SiCepat, J&T (selalu return 3 kurir) // Price: calculated based on weight
Fitur Mock Mode
| Fitur | Deskripsi |
|---|---|
| Dynamic Response | ID random, harga calculated berdasarkan berat |
| Data Persistence | Order tersimpan di cache untuk retrieve/cancel |
| Validasi Input | Same validation rules seperti real API |
| Error Simulation | Test error handling dengan toggle env |
| COD Support | Mock mode mendukung cash on delivery |
Switch ke Production
# Production BITESHIP_MOCK_MODE=false BITESHIP_API_KEY=biteship_live.your_api_key_here
Tidak perlu mengubah controller atau model - hanya toggle env variable!
Default Origin & Shipper
Jika toko Anda selalu mengirim dari lokasi yang sama, Anda bisa mengatur default origin dan default shipper di .env untuk menghindari pengulangan kode.
Environment Variables
| Variable | Keterangan | Contoh |
|---|---|---|
BITESHIP_ORIGIN_AREA_ID |
Area ID lokasi pickup (prioritas) | IDNP6IDNC148... |
BITESHIP_ORIGIN_POSTAL_CODE |
Kode pos lokasi pickup (fallback) | 12440 |
BITESHIP_ORIGIN_CONTACT_NAME |
Nama kontak di lokasi pickup | Budi |
BITESHIP_ORIGIN_CONTACT_PHONE |
Telepon kontak pickup | 08123456789 |
BITESHIP_ORIGIN_CONTACT_EMAIL |
Email kontak pickup (opsional) | warehouse@toko.com |
BITESHIP_ORIGIN_ADDRESS |
Alamat lengkap pickup | Jl. Sudirman No.1, Jakarta |
BITESHIP_ORIGIN_NOTE |
Catatan untuk kurir (opsional) | Depan gerbang hitam |
BITESHIP_SHIPPER_CONTACT_NAME |
Nama brand/toko di label | Toko Elektronik |
BITESHIP_SHIPPER_CONTACT_PHONE |
Telepon brand di label | 021-12345678 |
BITESHIP_SHIPPER_CONTACT_EMAIL |
Email brand di label (opsional) | support@toko.com |
BITESHIP_SHIPPER_ORGANIZATION |
Nama organisasi di label (opsional) | PT Toko Elektronik |
Penggunaan di Controller
use Aliziodev\Biteship\DTOs\Order\OrderRequest; use Aliziodev\Biteship\DTOs\Rate\RateRequest; // Order dengan default origin & shipper $orderRequest = (new OrderRequest) ->defaultOrigin() // Auto-fill dari BITESHIP_ORIGIN_* ->defaultShipper() // Auto-fill dari BITESHIP_SHIPPER_* ->destinationAreaId('IDNP20001') ->destinationContact('Ani', '08987654321') ->destinationAddress('Jl. Merdeka No.10, Bandung') ->courier('jne', 'REG') ->addItem(['name' => 'Laptop', 'value' => 5000000, 'weight' => 2000, 'quantity' => 1]); // Cek ongkir dengan default origin $rateRequest = (new RateRequest) ->defaultOrigin() // Auto-fill dari BITESHIP_ORIGIN_* ->destinationAreaId('IDNP20001') ->destinationContact('Ani', '08987654321') ->destinationAddress('Jl. Merdeka No.10') ->addItem(['name' => 'Laptop', 'value' => 5000000, 'weight' => 2000, 'quantity' => 1]); // Override jika perlu origin berbeda $orderRequest = (new OrderRequest) ->defaultOrigin() ->originAreaId('IDNP30001') // Override area_id untuk order ini saja ->destinationAreaId('IDNP20001')...
💡 Tips: Pastikan
defaultOrigin()dan destination menggunakan metode lokasi yang sama:
- Jika config pakai
BITESHIP_ORIGIN_AREA_ID→ destination pakaidestinationAreaId()- Jika config pakai
BITESHIP_ORIGIN_POSTAL_CODE→ destination pakaidestinationPostalCode()- Mixing methods (area_id + postal_code) akan menyebabkan error dari API Biteship.
Penggunaan
Cek Ongkir
use Aliziodev\Biteship\Facades\Biteship; use Aliziodev\Biteship\DTOs\Rate\RateRequest; $request = (new RateRequest) ->originAreaId('IDNP10001') ->originContact('Budi', '08123456789') ->originAddress('Jl. Sudirman No.1') ->destinationAreaId('IDNP20001') ->destinationContact('Ani', '08987654321') ->destinationAddress('Jl. Merdeka No.10') ->addItem(['name' => 'Baju', 'value' => 100000, 'weight' => 500, 'quantity' => 1]); $response = Biteship::rates()->check($request); // Ambil rate termurah $cheapest = $response->cheapest(); echo $cheapest->courierCode; // misal: 'sicepat' echo $cheapest->price; // misal: 12000 // Filter berdasarkan kurir $jneRates = $response->byCourier('jne'); // Ambil hanya kurir yang support COD $codRates = $response->codAvailable();
⚠️ Important: Origin dan destination harus menggunakan metode lokasi yang sama:
- ✅
area_id+area_id(High accuracy - Recommended)- ✅
postal_code+postal_code(Medium accuracy - Easy implementation)- ✅
coordinate+coordinate(For instant delivery)- ❌
area_id+postal_code(Error - inconsistent method)- ❌
postal_code+area_id(Error - inconsistent method)Jika menggunakan
defaultOrigin(), pastikan config origin (area_id/postal_code) cocok dengan destination method yang Anda gunakan.
Lewati Cache
Untuk operasi kritis yang membutuhkan data terbaru:
$response = Biteship::rates()->fresh()->check($request);
Hapus Cache
Biteship::rates()->forget($request);
Membuat Order
use Aliziodev\Biteship\DTOs\Order\OrderRequest; use Aliziodev\Biteship\Enums\OrderStatus; $request = (new OrderRequest) ->originAreaId('IDNP10001') ->originContact('Ali Sender', '08123456789') ->originAddress('Jl. Sudirman No.1') ->destinationAreaId('IDNP20001') ->destinationContact('Ani Recipient', '08987654321') ->destinationAddress('Jl. Merdeka No.10') ->courier('jne', 'REG') ->addItem(['name' => 'Sepatu Olahraga', 'value' => 250000, 'weight' => 800, 'quantity' => 1]); $order = Biteship::orders()->create($request); echo $order->id; // ID order Biteship echo $order->status; // enum OrderStatus echo $order->price; // Harga pengiriman
Shipper Fields (Branding/Labeling)
Biteship API mendukung shipper_* fields untuk branding pengirim pada shipping label. Shipper terpisah dari origin (lokasi pickup).
Via Environment Variables:
BITESHIP_SHIPPER_CONTACT_NAME="Toko Elektronik" BITESHIP_SHIPPER_CONTACT_PHONE="021-12345678" BITESHIP_SHIPPER_CONTACT_EMAIL="support@toko.com" BITESHIP_SHIPPER_ORGANIZATION="PT Toko Elektronik Indonesia"
Penggunaan:
// Auto-fill dari config $request = (new OrderRequest) ->defaultShipper() // Load dari .env ->originAreaId('IDNP10001') ->originContact('Budi', '08123456789') ->originAddress('Jl. Sudirman No.1') ->destinationAreaId('IDNP20001') ->destinationContact('Ani', '08987654321') ->destinationAddress('Jl. Merdeka No.10') ->courier('jne', 'REG') ->addItem(['name' => 'Laptop', 'value' => 5000000, 'weight' => 2000, 'quantity' => 1]);
Manual Set:
$request = (new OrderRequest) ->shipper('Toko Elektronik', '021-12345678', 'support@toko.com', 'PT Toko Elektronik') // atau set individual: ->shipperName('Toko Elektronik') ->shipperPhone('021-12345678') ->shipperEmail('support@toko.com') ->shipperOrganization('PT Toko Elektronik') ->originAreaId('IDNP10001')...
Generated Payload:
{
"shipper_contact_name": "Toko Elektronik",
"shipper_contact_phone": "021-12345678",
"shipper_contact_email": "support@toko.com",
"shipper_organization": "PT Toko Elektronik",
"origin_contact_name": "Budi",
"origin_contact_phone": "08123456789",
...
}
Mengambil Order
$order = Biteship::orders()->find('ORD-123456');
Membatalkan Order
$order = Biteship::orders()->cancel('ORD-123456', 'Pembeli membatalkan');
Melacak Pengiriman
// Lacak berdasarkan ID order Biteship $tracking = Biteship::tracking()->byOrderId('ORD-123456'); // Lacak berdasarkan resi (public tracking - works untuk kurir apapun) $tracking = Biteship::tracking()->byWaybill('JNE00123456789', 'jne');
Daftar Kurir
$couriers = Biteship::couriers()->all();
Search Area (Maps API)
Skenario: Form input alamat dengan autocomplete untuk memudahkan user memilih area pengiriman yang valid.
use Aliziodev\Biteship\Facades\Biteship; class AddressController extends Controller { public function searchArea(Request $request) { $request->validate([ 'query' => 'required|string|min:3', ]); // Search area berdasarkan input user $areas = Biteship::locations()->search( $request->query, $request->type ?? 'single' // 'single' atau 'all' ); return response()->json([ 'areas' => $areas->map(function ($area) { return [ 'id' => $area['id'], // Area ID untuk digunakan di RateRequest 'name' => $area['name'], 'description' => $area['description'], 'country' => $area['country'], 'province' => $area['province'], 'city' => $area['city'], 'type' => $area['type'], ]; }), ]); } }
Contoh response:
{
"areas": [
{
"id": "IDNP6IDNC148...",
"name": "Jakarta Selatan",
"description": "Jakarta Selatan, DKI Jakarta",
"country": "ID",
"province": "DKI Jakarta",
"city": "Jakarta Selatan",
"type": "single"
}
]
}
Mengelola Lokasi
use Aliziodev\Biteship\DTOs\Location\LocationRequest; // Buat lokasi $location = Biteship::locations()->create( (new LocationRequest) ->name('Apotik Gambir') ->contactName('Ahmad') ->contactPhone('08123456789') ->address('Jl. Gambir Selatan') ->postalCode('10110') ->coordinates(-6.232, 102.221) ->type('origin') ); // Cari lokasi $location = Biteship::locations()->find($id); // Update lokasi $location = Biteship::locations()->update($id, ['name' => 'Apotek Monas']); // Hapus lokasi Biteship::locations()->delete($id);
Generate Label Pengiriman
use Aliziodev\Biteship\Facades\Biteship; // Generate data label $label = Biteship::label()->generate($orderResponse); // Render sebagai HTML $html = Biteship::label()->render($orderResponse); // Return sebagai HTTP response return Biteship::label()->response($orderResponse);
Anda dapat publish dan mengkustomisasi view label:
php artisan vendor:publish --tag=biteship-views
Real Case Implementasi
Case 1: E-commerce Checkout dengan Cek Ongkir Real-time
Skenario: Toko online ingin menampilkan ongkir di halaman checkout saat user memilih alamat dan kurir.
use Aliziodev\Biteship\Facades\Biteship; use Aliziodev\Biteship\DTOs\Rate\RateRequest; class CheckoutController extends Controller { public function calculateShipping(Request $request) { $cart = Cart::where('user_id', auth()->id())->with('product')->get(); // Build request dari cart $rateRequest = (new RateRequest) ->originAreaId(config('store.origin_area_id')) ->originContact(config('store.contact_name'), config('store.contact_phone')) ->originAddress(config('store.address')) ->destinationPostalCode($request->postal_code) ->destinationContact($request->name, $request->phone) ->destinationAddress($request->address); // Tambahkan semua item dari cart foreach ($cart as $item) { $rateRequest->addItem([ 'name' => $item->product->name, 'value' => $item->product->price, 'weight' => $item->product->weight, 'quantity' => $item->quantity, ]); } // Cek ongkir (otomatis di-cache 15 menit) $response = Biteship::rates()->check($rateRequest); // Filter hanya kurir yang support COD jika user bayar COD if ($request->payment_method === 'cod') { $rates = $response->codAvailable(); } else { $rates = $response->pricing; } return response()->json([ 'rates' => $rates, 'cheapest' => $response->cheapest(), ]); } }
Case 2: Order dengan COD dan Asuransi
Skenario: Toko elektronik ingin mengirim barang dengan COD dan asuransi untuk barang bernilai tinggi.
use Aliziodev\Biteship\Facades\Biteship; use Aliziodev\Biteship\DTOs\Order\OrderRequest; class OrderController extends Controller { public function createOrder(Request $request) { $order = Order::create($request->validated()); // Buat order ke Biteship $biteshipRequest = (new OrderRequest) ->originAreaId(config('store.origin_area_id')) ->originContact('Toko Elektronik', '021-12345678') ->originAddress('Jl. Teknologi No. 10, Jakarta') ->destinationPostalCode($order->shipping_postal_code) ->destinationContact($order->recipient_name, $order->recipient_phone) ->destinationAddress($order->shipping_address) ->courier($request->courier_code, $request->courier_type); // Tambahkan item foreach ($order->items as $item) { $biteshipRequest->addItem([ 'name' => $item->product_name, 'value' => $item->price, 'weight' => $item->weight, 'quantity' => $item->quantity, ]); } // Tambahkan asuransi jika barang bernilai > 1 juta if ($order->total_value > 1000000) { $biteshipRequest->withInsurance($order->total_value); } // Tambahkan COD jika pembayaran COD if ($order->payment_method === 'cod') { $biteshipRequest->withCOD($order->total_amount, '7_days'); } try { $biteshipOrder = Biteship::orders()->create($biteshipRequest); // Simpan ID order Biteship $order->update([ 'biteship_order_id' => $biteshipOrder->id, 'shipping_cost' => $biteshipOrder->price, 'status' => 'processing', ]); return redirect()->route('orders.show', $order) ->with('success', 'Order berhasil dibuat'); } catch (RateLimitException $e) { // Retry dengan queue job dispatch(new CreateBiteshipOrderJob($order, $biteshipRequest)) ->delay(now()->addSeconds($e->retryAfter() ?? 5)); return back()->with('warning', 'Order sedang diproses'); } } }
Case 3: Auto-update Status Order via Webhook
Skenario: Sistem ingin otomatis update status order dan kirim notifikasi ke customer saat status pengiriman berubah.
// app/Listeners/UpdateOrderStatusFromWebhook.php namespace App\Listeners; use Aliziodev\Biteship\Events\OrderStatusUpdated; use App\Models\Order; use App\Notifications\ShippingStatusNotification; class UpdateOrderStatusFromWebhook { public function handle(OrderStatusUpdated $event) { // Cari order berdasarkan Biteship order ID $order = Order::where('biteship_order_id', $event->payload->orderId)->first(); if (! $order) { return; } // Update status order $oldStatus = $order->shipping_status; $order->update(['shipping_status' => $event->payload->status]); // Kirim notifikasi jika status berubah if ($oldStatus !== $event->payload->status) { $order->user->notify(new ShippingStatusNotification($order)); // Jika status delivered, auto-complete order if ($event->payload->status === 'delivered') { $order->update(['status' => 'completed']); } // Jika status returned, buat refund request if ($event->payload->status === 'returned') { dispatch(new CreateRefundRequestJob($order)); } } } }
Case 4: Tracking Page untuk Customer
Skenario: Halaman tracking untuk customer melihat status pengiriman secara real-time.
use Aliziodev\Biteship\Facades\Biteship; class TrackingController extends Controller { public function show($orderId) { $order = Order::with('items')->findOrFail($orderId); // Ambil data tracking terbaru dari Biteship $tracking = Biteship::tracking()->byOrderId($order->biteship_order_id); return view('tracking.show', [ 'order' => $order, 'tracking' => $tracking, 'history' => $tracking->history, // Array history status 'currentStatus' => $tracking->status, ]); } }
Case 5: Public Tracking untuk Resi Apapun
Skenario: Customer ingin melacak paket dengan nomor resi dari kurir manapun (tidak harus order dari toko kita).
class PublicTrackingController extends Controller { public function track(Request $request) { $request->validate([ 'waybill' => 'required|string', 'courier' => 'required|string|in:jne,sicepat,jnt,anteraja', ]); try { $tracking = Biteship::tracking()->byWaybill( $request->waybill, $request->courier ); return response()->json([ 'success' => true, 'data' => [ 'waybill' => $tracking->waybillId, 'status' => $tracking->status, 'history' => $tracking->history, ], ]); } catch (ApiException $e) { return response()->json([ 'success' => false, 'message' => 'Resi tidak ditemukan atau kurir salah', ], 404); } } }
Case 6: Autocomplete Alamat di Form Checkout
Skenario: Form checkout dengan autocomplete alamat untuk memudahkan user memilih area yang valid dan mendapatkan area_id yang akurat.
use Aliziodev\Biteship\Facades\Biteship; class CheckoutController extends Controller { public function searchAddress(Request $request) { $query = $request->get('q'); if (strlen($query) < 3) { return response()->json(['areas' => []]); } // Search area dengan Maps API $areas = Biteship::locations()->search($query, 'single'); return response()->json([ 'areas' => $areas->take(10)->map(function ($area) { return [ 'id' => $area['id'], 'text' => $area['description'], 'province' => $area['province'], 'city' => $area['city'], ]; }), ]); } public function calculateShipping(Request $request) { // Gunakan area_id dari hasil autocomplete $rateRequest = (new RateRequest) ->originAreaId(config('store.origin_area_id')) ->originContact(config('store.contact_name'), config('store.contact_phone')) ->originAddress(config('store.address')) ->destinationAreaId($request->destination_area_id) // Area ID dari autocomplete ->destinationContact($request->name, $request->phone) ->destinationAddress($request->address); // Tambahkan item dari cart foreach (Cart::items() as $item) { $rateRequest->addItem([ 'name' => $item->name, 'value' => $item->price, 'weight' => $item->weight, 'quantity' => $item->quantity, ]); } $response = Biteship::rates()->check($rateRequest); return response()->json([ 'rates' => $response->pricing, 'cheapest' => $response->cheapest(), ]); } }
Frontend (JavaScript/Vue/React):
// Contoh implementasi dengan debounce async function searchAddress(query) { if (query.length < 3) return []; const response = await fetch(`/checkout/search-address?q=${query}`); const data = await response.json(); return data.areas; } // Gunakan area_id yang dipilih untuk cek ongkir async function calculateShipping(areaId) { const response = await fetch('/checkout/shipping', { method: 'POST', body: JSON.stringify({ destination_area_id: areaId }), }); return await response.json(); }
Case 7: Display Daftar Kurir di Frontend
Skenario: Halaman checkout ingin menampilkan daftar kurir yang tersedia dengan logo dan informasi layanan.
use Aliziodev\Biteship\Facades\Biteship; class CourierController extends Controller { public function index() { // Ambil semua kurir yang tersedia $couriers = Biteship::couriers()->all(); // Group berdasarkan kode kurir untuk kemudahan display $grouped = $couriers->groupBy('code'); return response()->json([ 'couriers' => $grouped->map(function ($items, $code) { return [ 'code' => $code, 'name' => $items->first()->name, 'logo' => $items->first()->logo_url, 'services' => $items->map(function ($item) { return [ 'type' => $item->type, 'name' => $item->service_name, 'description' => $item->description, ]; })->values(), ]; })->values(), ]); } }
Case 8: Manajemen Lokasi untuk Multi-warehouse
Skenario: Toko dengan beberapa gudang ingin menyimpan lokasi gudang sebagai origin yang bisa digunakan berulang.
class WarehouseController extends Controller { public function store(Request $request) { // Simpan lokasi gudang ke Biteship $location = Biteship::locations()->create( (new LocationRequest) ->name($request->name) ->contactName($request->contact_name) ->contactPhone($request->contact_phone) ->address($request->address) ->postalCode($request->postal_code) ->coordinates($request->latitude, $request->longitude) ->type('origin') ); // Simpan ID lokasi Biteship ke database lokal Warehouse::create([ 'name' => $request->name, 'biteship_location_id' => $location->id, 'address' => $request->address, // ... ]); return back()->with('success', 'Gudang berhasil ditambahkan'); } public function update(Request $request, $id) { $warehouse = Warehouse::findOrFail($id); // Update lokasi di Biteship Biteship::locations()->update( $warehouse->biteship_location_id, [ 'name' => $request->name, 'contact_name' => $request->contact_name, 'contact_phone' => $request->contact_phone, 'address' => $request->address, ] ); // Update lokal $warehouse->update($request->only(['name', 'address'])); return back()->with('success', 'Gudang berhasil diupdate'); } }
Case 9: Generate dan Print Label Pengiriman
Skenario: Admin ingin generate label PDF untuk dicetak dan ditempel di paket.
class ShippingLabelController extends Controller { public function generate($orderId) { $order = Order::with('biteshipOrder')->findOrFail($orderId); // Ambil data order terbaru dari Biteship $biteshipOrder = Biteship::orders()->find($order->biteship_order_id); // Generate label HTML $html = Biteship::label()->render($biteshipOrder); // Konversi ke PDF (gunakan dompdf/snappy) $pdf = app('pdf')->loadHTML($html); return $pdf->download("label-{$order->id}.pdf"); } public function print($orderId) { $order = Order::with('biteshipOrder')->findOrFail($orderId); $biteshipOrder = Biteship::orders()->find($order->biteship_order_id); // Return HTML response untuk print langsung return Biteship::label()->response($biteshipOrder); } }
Case 10: Cancellation Order dengan Validasi
Skenario: Customer ingin batalkan order, tapi hanya bisa dibatalkan jika status masih confirmed, scheduled, atau allocated.
class OrderCancellationController extends Controller { public function cancel(Request $request, $orderId) { $order = Order::findOrFail($orderId); // Validasi: hanya bisa batalkan jika belum dipickup if (! in_array($order->shipping_status, ['confirmed', 'scheduled', 'allocated'])) { return back()->with('error', 'Order tidak dapat dibatalkan karena sudah diproses kurir'); } try { // Batalkan di Biteship $cancelledOrder = Biteship::orders()->cancel( $order->biteship_order_id, $request->reason ?? 'Dibatalkan oleh customer' ); // Update status lokal $order->update([ 'status' => 'cancelled', 'shipping_status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => $request->reason, ]); // Proses refund jika perlu if ($order->payment_method === 'midtrans') { dispatch(new ProcessRefundJob($order)); } return back()->with('success', 'Order berhasil dibatalkan'); } catch (ApiException $e) { return back()->with('error', 'Gagal membatalkan order: ' . $e->getMessage()); } } }
Webhooks
Setup Webhook
-
Konfigurasi URL webhook di dashboard Biteship:
https://your-app.com/biteship/webhook -
Opsional konfigurasi verifikasi signature di dashboard Biteship:
- Headers Signature Key:
X-Biteship-Signature(atau nama custom) - Headers Signature Secret:
your-secret-value
- Headers Signature Key:
-
Tambahkan environment variable yang sesuai:
BITESHIP_WEBHOOK_SIGNATURE_KEY=X-Biteship-Signature BITESHIP_WEBHOOK_SIGNATURE_SECRET=your-secret-value
Menangani Event Webhook
Package mendispatch event Laravel untuk setiap tipe webhook. Dengarkan di EventServiceProvider Anda:
use Aliziodev\Biteship\Events\OrderStatusUpdated; use Aliziodev\Biteship\Events\OrderPriceUpdated; use Aliziodev\Biteship\Events\OrderWaybillUpdated; protected $listen = [ OrderStatusUpdated::class => [ SendShippingNotification::class, UpdateOrderStatus::class, ], OrderPriceUpdated::class => [ HandlePriceChange::class, ], OrderWaybillUpdated::class => [ UpdateWaybillNumber::class, ], ];
Payload Event
Setiap event berisi payload DTO dengan data yang relevan:
Event::listen(OrderStatusUpdated::class, function ($event) { $orderId = $event->payload->orderId; $status = $event->payload->status; // Handle update status });
Exception Handling
Package menyediakan exception terstruktur untuk skenario error berbeda:
use Aliziodev\Biteship\Exceptions\AuthenticationException; use Aliziodev\Biteship\Exceptions\RateLimitException; use Aliziodev\Biteship\Exceptions\ValidationException; use Aliziodev\Biteship\Exceptions\ApiException; try { $rates = Biteship::rates()->check($request); } catch (AuthenticationException $e) { // API key tidak valid } catch (RateLimitException $e) { // Rate limit terlampaui - gunakan retryAfter() untuk backoff $retryAfter = $e->retryAfter(); // detik untuk menunggu } catch (ValidationException $e) { // Data request tidak valid } catch (ApiException $e) { // Error API lainnya }
Penanganan Rate Limit
try { $rates = Biteship::rates()->check($request); } catch (RateLimitException $e) { // Retry setelah waktu yang ditentukan dispatch(new CheckRatesJob($request)) ->delay(now()->addSeconds($e->retryAfter() ?? 5)); }
Testing
Jalankan test suite:
composer test
Atau menggunakan Pest langsung:
./vendor/bin/pest
Layer Database (Opsional)
Jika Anda memilih menggunakan layer database opsional saat instalasi:
-
Jalankan migrasi:
php artisan migrate
-
Tambahkan trait
HasBiteshipke model Order Anda:use Aliziodev\Biteship\Concerns\HasBiteship; class Order extends Model { use HasBiteship; }
-
Gunakan helper methods:
$order->biteshipOrder; // Relasi ke biteship_orders $order->biteship_status; // Shortcut attribute $order->biteship_waybill_id; // Shortcut attribute
Batas Rate API
Biteship memiliki batas rate berikut (berdasarkan dokumentasi):
| API | Production | Sandbox |
|---|---|---|
| Maps | 50 req/s | 5 req/s |
| Rates | 20 req/s | 5 req/s |
| Location | 10 req/s | 5 req/s |
| Order | 20 req/s | 5 req/s |
| Tracking | 50 req/s | 5 req/s |
Rate caching bawaan membantu mengurangi risiko mencapai batas ini.
Keamanan
- Verifikasi signature webhook menggunakan
hash_equals()untuk perbandingan yang aman dari timing attack - API key disimpan di environment variables
- Semua request HTTP menggunakan konfigurasi timeout yang tepat
- Exception terstruktur mencegah kebocoran data sensitif
Changelog
Silakan lihat CHANGELOG.md untuk perubahan terbaru.
Kontribusi
Kontribusi sangat diterima! Silakan submit Pull Request.
Lisensi
The MIT License (MIT). Silakan lihat LICENSE untuk informasi lebih lanjut.
Dukungan
Jika Anda mengalami masalah atau memiliki pertanyaan, silakan buka issue di GitHub.