Aplikasi E-Commerce Laravel 6 #9: Customer Transactions

Aplikasi E-Commerce Laravel 6 #9: Customer Transactions

Pendahuluan

Mencatat data pesanan pelanggan yang sebelumnya telah disimpan ke dalam cookie, kini akan kita lengkapi dengan data pribadi pelanggan terkait untuk kemudian disimpan secara pasti ke dalam database. Proses transaksi ini membutuhkan effort berlebih karena kita akan berinteraksi dengan beberapa table sehingga proses ini terbilang cukup kompleks.

Schema yang diinginkan adalah ketika user telah menambahkan produk yang diinginkan ke dalam keranjang, maka pada halaman keranjang terdapat tombol untuk mengarahkannya ke halaman form transaksi sehingga calon pelanggan bisa memasukkan informasi terkait data dirinya.

Baca Juga: Aplikasi E-commerce Laravel 6 #8: Manage Carts

Fix Carts Issue

Terdapat sedikit kendala ketika data di cookie kosong yang berarti hasilnya akan null, maka akan terjadi error pada halaman keranjang belanja. Untuk mengatasi masalah ini sebenarnya cukup dengan menggunakan ternary operator pada php untuk mengecek jika datanya tidak sama dengan null (dan atau kosong) maka data tersebut akan disimpan kedalam variable, tapi jika berlaku sebaliknya maka kita berikan array kosong.

Karena fungsi ini cukup sering digunakan, maka akan kita pisahkan ke dalam sebuah fungsi tersendiri agar code dengan logic yang sama tidak dituliskan secara berulang. Buka file CartController.php dan tambahkan method berikut.

private function getCarts()
{
    $carts = json_decode(request()->cookie('dw-carts'), true);
    $carts = $carts != '' ? $carts:[];
    return $carts;
}

Masih dengan file yang sama, replace semua code $carts = json_decode(request()->cookie('dw-carts'), true); menjadi

$carts = $this->getCarts();

Form Transactions

Menyiapkan form transaksi adalah langkah selanjutnya dari proses membuat aplikasi e-commerce menggunakan Laravel 6, dimana fungsinya adalah untuk mencatat pesanan yang akan dilakukan oleh pelanggan. Adapun data produk yang akan dipesan telah disimpan ke dalam Cookie yang selanjutnya akan dimasukkan ke dalam database, kemudian data yang ada pada cookie akan dibersihkan.

Langkah pertama adalah membuat tampilannya, buka file CartController.php dan tambahkan method berikut.

public function checkout()
{
    //QUERY UNTUK MENGAMBIL SEMUA DATA PROPINSI
    $provinces = Province::orderBy('created_at', 'DESC')->get();
    $carts = $this->getCarts(); //MENGAMBIL DATA CART
    //MENGHITUNG SUBTOTAL DARI KERANJANG BELANJA (CART)
    $subtotal = collect($carts)->sum(function($q) {
        return $q['qty'] * $q['product_price'];
    });
    //ME-LOAD VIEW CHECKOUT.BLADE.PHP DAN PASSING DATA PROVINCES, CARTS DAN SUBTOTAL
    return view('ecommerce.checkout', compact('provinces', 'carts', 'subtotal'));
}

Jangan lupa untuk menambahkan use statement.

use App\Province;

Kemudian buat sebuah file bernama checkout.blade.php dan letakkan ke dalam folder resources/views/ecommerce. Masukkan code berikut untuk meng-handle form transaksi

@extends('layouts.ecommerce')

@section('title')
    <title>Checkout - Dw Ecommerce</title>
@endsection

@section('content')
    <!--================Home Banner Area =================-->
	<section class="banner_area">
		<div class="banner_inner d-flex align-items-center">
			<div class="overlay"></div>
			<div class="container">
				<div class="banner_content text-center">
					<h2>Informasi Pengiriman</h2>
					<div class="page_link">
            <a href="{{ url('/') }}">Home</a>
						<a href="#">Checkout</a>
					</div>
				</div>
			</div>
		</div>
	</section>
	<!--================End Home Banner Area =================-->

	<!--================Checkout Area =================-->
	<section class="checkout_area section_gap">
		<div class="container">
			<div class="billing_details">
				<div class="row">
					<div class="col-lg-8">
            <h3>Informasi Pengiriman</h3>          
              @if (session('error'))
                  <div class="alert alert-danger">{{ session('error') }}</div>
              @endif
                        
              
            	<!-- REMOVE DULU VALUE ACTION-NYA JIKA INGIN MELIHATNYA DI BROWSER -->
            	<!-- KARENA ROUTE NAME front.store_checkout BELUM DIBUAT -->
              <form class="row contact_form" action="{{ route('front.store_checkout') }}" method="post" novalidate="novalidate">
                            @csrf
                        <div class="col-md-12 form-group p_star">
                            <label for="">Nama Lengkap</label>
                            <input type="text" class="form-control" id="first" name="customer_name" required>
                            
                            <!-- UNTUK MENAMPILKAN JIKA TERDAPAT ERROR VALIDASI -->
                            <p class="text-danger">{{ $errors->first('customer_name') }}</p>
                        </div>
                        <div class="col-md-6 form-group p_star">
                            <label for="">No Telp</label>
                            <input type="text" class="form-control" id="number" name="customer_phone" required>
                            <p class="text-danger">{{ $errors->first('customer_phone') }}</p>
                        </div>
                        <div class="col-md-6 form-group p_star">
                            <label for="">Email</label>
                            <input type="email" class="form-control" id="email" name="email" required>
                            <p class="text-danger">{{ $errors->first('email') }}</p>
                        </div>
                        <div class="col-md-12 form-group p_star">
                            <label for="">Alamat Lengkap</label>
                            <input type="text" class="form-control" id="add1" name="customer_address" required>
                            <p class="text-danger">{{ $errors->first('customer_address') }}</p>
                        </div>
                        <div class="col-md-12 form-group p_star">
                            <label for="">Propinsi</label>
                            <select class="form-control" name="province_id" id="province_id" required>
                                <option value="">Pilih Propinsi</option>
                              	<!-- LOOPING DATA PROVINCE UNTUK DIPILIH OLEH CUSTOMER -->
                                @foreach ($provinces as $row)
                                <option value="{{ $row->id }}">{{ $row->name }}</option>
                                @endforeach
                            </select>
                            <p class="text-danger">{{ $errors->first('province_id') }}</p>
                        </div>
                
                  <!-- ADAPUN DATA KOTA DAN KECAMATAN AKAN DI RENDER SETELAH PROVINSI DIPILIH -->
                        <div class="col-md-12 form-group p_star">
                            <label for="">Kabupaten / Kota</label>
                            <select class="form-control" name="city_id" id="city_id" required>
                                <option value="">Pilih Kabupaten/Kota</option>
                            </select>
                            <p class="text-danger">{{ $errors->first('city_id') }}</p>
                        </div>
                        <div class="col-md-12 form-group p_star">
                            <label for="">Kecamatan</label>
                            <select class="form-control" name="district_id" id="district_id" required>
                                <option value="">Pilih Kecamatan</option>
                            </select>
                            <p class="text-danger">{{ $errors->first('district_id') }}</p>
                        </div>
                <!-- ADAPUN DATA KOTA DAN KECAMATAN AKAN DI RENDER SETELAH PROVINSI DIPILIH -->
                    
					</div>
					<div class="col-lg-4">
						<div class="order_box">
							<h2>Ringkasan Pesanan</h2>
							<ul class="list">
								<li>
									<a href="#">Product
										<span>Total</span>
									</a>
                </li>
                @foreach ($carts as $cart)
								<li>
									<a href="#">{{ \Str::limit($cart['product_name'], 10) }}
                    <span class="middle">x {{ $cart['qty'] }}</span>
                    <span class="last">Rp {{ number_format($cart['product_price']) }}</span>
									</a>
                </li>
                @endforeach
							</ul>
							<ul class="list list_2">
								<li>
									<a href="#">Subtotal
                    <span>Rp {{ number_format($subtotal) }}</span>
									</a>
								</li>
								<li>
									<a href="#">Pengiriman
										<span>Rp 0</span>
									</a>
								</li>
								<li>
									<a href="#">Total
										<span>Rp {{ number_format($subtotal) }}</span>
									</a>
								</li>
							</ul>
              <button class="main_btn">Bayar Pesanan</button>
              </form>
						</div>
					</div>
				</div>
			</div>
		</div>
	</section>
	<!--================End Checkout Area =================-->
@endsection

Definisikan route-nya agar bisa diakses, buka file routes/web.php dan tambahkan code berikut

Route::get('/checkout', 'Ecommerce\CartController@checkout')->name('front.checkout');

Langkah terakhir pada sub-bab ini adalah dengan menghubungkan tombol Proceed to checkout ke halaman checkout. Buka file cart.blade.php dan cari kalimat Proceed to checkout kemudian modifikasi tag-nya menjadi.

<a class="gray_btn" href="{{ route('front.product') }}">Continue Shopping</a>
<a class="main_btn" href="{{ route('front.checkout') }}">Proceed to checkout</a>

Dynamic Option With Ajax

Ketika select box province dipilih maka secara otomatis akan me-render data kabupaten atau kota pada select box berikutnya. Tugas kita kali ini adalah membuat fitur tersebut dengan memanfaatkan ajax pada jquery.

Buka file resources/views/layouts/ecommerce.blade.php dan tambahkan code di bawah ini tepat sebelum tag </body>

@yield('js')

Gunanya adalah view yang me-load layouts diatas bisa menambahkan custom javascript khusus pada halaman yang sedang menggunakannya, cara kerja-nya sama dengan cara kerja @yield('content') dan sebagainya.

Kembali pada file checkout.blade.php, tambahkan code berikut pada line atau baris terakhir

@section('js')
    <script>
        //KETIKA SELECT BOX DENGAN ID province_id DIPILIH
        $('#province_id').on('change', function() {
            //MAKA AKAN MELAKUKAN REQUEST KE URL /API/CITY
            //DAN MENGIRIMKAN DATA PROVINCE_ID
            $.ajax({
                url: "{{ url('/api/city') }}",
                type: "GET",
                data: { province_id: $(this).val() },
                success: function(html){
                    //SETELAH DATA DITERIMA, SELEBOX DENGAN ID CITY_ID DI KOSONGKAN
                    $('#city_id').empty()
                    //KEMUDIAN APPEND DATA BARU YANG DIDAPATKAN DARI HASIL REQUEST VIA AJAX
                    //UNTUK MENAMPILKAN DATA KABUPATEN / KOTA
                    $('#city_id').append('<option value="">Pilih Kabupaten/Kota</option>')
                    $.each(html.data, function(key, item) {
                        $('#city_id').append('<option value="'+item.id+'">'+item.name+'</option>')
                    })
                }
            });
        })

        //LOGICNYA SAMA DENGAN CODE DIATAS HANYA BERBEDA OBJEKNYA SAJA
        $('#city_id').on('change', function() {
            $.ajax({
                url: "{{ url('/api/district') }}",
                type: "GET",
                data: { city_id: $(this).val() },
                success: function(html){
                    $('#district_id').empty()
                    $('#district_id').append('<option value="">Pilih Kecamatan</option>')
                    $.each(html.data, function(key, item) {
                        $('#district_id').append('<option value="'+item.id+'">'+item.name+'</option>')
                    })
                }
            });
        })
    </script>
@endsection

Ada dua request dari code di atas, yakni: /api/city dan /api/district. Kita lengkapi satu persatu, buka file CartController.php dan tambahkan method

public function getCity()
{
    //QUERY UNTUK MENGAMBIL DATA KOTA / KABUPATEN BERDASARKAN PROVINCE_ID
    $cities = City::where('province_id', request()->province_id)->get();
    //KEMBALIKAN DATANYA DALAM BENTUK JSON
    return response()->json(['status' => 'success', 'data' => $cities]);
}

public function getDistrict()
{
    //QUERY UNTUK MENGAMBIL DATA KECAMATAN BERDASARKAN CITY_ID
    $districts = District::where('city_id', request()->city_id)->get();
    //KEMUDIAN KEMBALIKAN DATANYA DALAM BENTUK JSON
    return response()->json(['status' => 'success', 'data' => $districts]);
}

Jangan lupa untuk menambahkan use statement.

use App\City;
use App\District;

Kemudian definisikan routing-nya, buka file routes/api.php dan tambahkan code

Route::get('city', 'Ecommerce\CartController@getCity'); //ROUTE API UNTUK /CITY
Route::get('district', 'Ecommerce\CartController@getDistrict'); //ROUTE API UNTUK /DISTRICT

Bagian terakhir ada memodifikasi file public/ecommerce/js/theme.js dan hapus code.

$('select').niceSelect();

Make Transactions

Form transaksi telah tersedia, dimana tampilan tersebut memungkinkan user untuk memasukkan informasi pesanannya yang meliputi nama pelanggan, alamat dan sebagainya. Pada bagian ini tugas kita adalah meng-handle data yang dikirimkan dari form tersebut. Buka file CartController.php dan tambahkan method berikut

public function processCheckout(Request $request)
{
    //VALIDASI DATANYA
    $this->validate($request, [
        'customer_name' => 'required|string|max:100',
        'customer_phone' => 'required',
        'email' => 'required|email',
        'customer_address' => 'required|string',
        'province_id' => 'required|exists:provinces,id',
        'city_id' => 'required|exists:cities,id',
        'district_id' => 'required|exists:districts,id'
    ]);

    //INISIASI DATABASE TRANSACTION
    //DATABASE TRANSACTION BERFUNGSI UNTUK MEMASTIKAN SEMUA PROSES SUKSES UNTUK KEMUDIAN DI COMMIT AGAR DATA BENAR BENAR DISIMPAN, JIKA TERJADI ERROR MAKA KITA ROLLBACK AGAR DATANYA SELARAS
    DB::beginTransaction();
    try {
        //CHECK DATA CUSTOMER BERDASARKAN EMAIL
        $customer = Customer::where('email', $request->email)->first();
        //JIKA DIA TIDAK LOGIN DAN DATA CUSTOMERNYA ADA
        if (!auth()->check() && $customer) {
            //MAKA REDIRECT DAN TAMPILKAN INSTRUKSI UNTUK LOGIN 
            return redirect()->back()->with(['error' => 'Silahkan Login Terlebih Dahulu']);
        }

        //AMBIL DATA KERANJANG
        $carts = $this->getCarts();
        //HITUNG SUBTOTAL BELANJAAN
        $subtotal = collect($carts)->sum(function($q) {
            return $q['qty'] * $q['product_price'];
        });

        //SIMPAN DATA CUSTOMER BARU
        $customer = Customer::create([
            'name' => $request->customer_name,
            'email' => $request->email,
            'phone_number' => $request->customer_phone,
            'address' => $request->customer_address,
            'district_id' => $request->district_id,
            'status' => false
        ]);

        //SIMPAN DATA ORDER
        $order = Order::create([
            'invoice' => Str::random(4) . '-' . time(), //INVOICENYA KITA BUAT DARI STRING RANDOM DAN WAKTU
            'customer_id' => $customer->id,
            'customer_name' => $customer->name,
            'customer_phone' => $request->customer_phone,
            'customer_address' => $request->customer_address,
            'district_id' => $request->district_id,
            'subtotal' => $subtotal
        ]);

        //LOOPING DATA DI CARTS
        foreach ($carts as $row) {
            //AMBIL DATA PRODUK BERDASARKAN PRODUCT_ID
            $product = Product::find($row['product_id']);
            //SIMPAN DETAIL ORDER
            OrderDetail::create([
                'order_id' => $order->id,
                'product_id' => $row['product_id'],
                'price' => $row['product_price'],
                'qty' => $row['qty'],
                'weight' => $product->weight
            ]);
        }
        
        //TIDAK TERJADI ERROR, MAKA COMMIT DATANYA UNTUK MENINFORMASIKAN BAHWA DATA SUDAH FIX UNTUK DISIMPAN
        DB::commit();

        $carts = [];
        //KOSONGKAN DATA KERANJANG DI COOKIE
        $cookie = cookie('dw-carts', json_encode($carts), 2880);
        //REDIRECT KE HALAMAN FINISH TRANSAKSI
        return redirect(route('front.finish_checkout', $order->invoice))->cookie($cookie);
    } catch (\Exception $e) {
        //JIKA TERJADI ERROR, MAKA ROLLBACK DATANYA
        DB::rollback();
        //DAN KEMBALI KE FORM TRANSAKSI SERTA MENAMPILKAN ERROR
        return redirect()->back()->with(['error' => $e->getMessage()]);
    }
}

Jangan lupa untuk menambahkan use statement

use App\Customer;
use App\Order;
use App\OrderDetail;
use Illuminate\Support\Str;
use DB;

Definisikan routing-nya, buka file routes/web.php dan tambahkan route berikut

Route::post('/checkout', 'Ecommerce\CartController@processCheckout')->name('front.store_checkout');

Pastikan untuk mengizinkan penyimpanan data melalui Eloquent, buka file Customer.php dan modifikasi menjadi

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    protected $guarded = [];
}

Buka file Order.php dan modifikasi juga menjadi.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected $guarded = [];
    
    //MEMBUAT RELASI KE MODEL DISTRICT.PHP
    public function district()
    {
        return $this->belongsTo(District::class);
    }
}

Selanjutnya buka file OrderDetail.php dan modifikasi menjadi

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class OrderDetail extends Model
{
    protected $guarded = [];
}

Bagian terakhir dari seri modifikasi model adalah file District.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class District extends Model
{
    //BUAT RELASI KE MODEL CITY.PHP
    public function city()
    {
        return $this->belongsTo(City::class);
    }
}

Setelah proses penyimpanan berhasil, maka prosesnya akan diarahkan ke halaman yang berfungsi untuk menampilkan summary pesanan. Buka file CartController.php dan tambahkan method

public function checkoutFinish($invoice)
{
    //AMBIL DATA PESANAN BERDASARKAN INVOICE
    $order = Order::with(['district.city'])->where('invoice', $invoice)->first();
    //LOAD VIEW checkout_finish.blade.php DAN PASSING DATA ORDER
    return view('ecommerce.checkout_finish', compact('order'));
}

Buat file checkout_finish.blade.php dan tambahkan code berikut yang berisi informasi pesanan yang baru saja dilakukan.

@extends('layouts.ecommerce')

@section('title')
    <title>Keranjang Belanja - Dw Ecommerce</title>
@endsection

@section('content')
    <!--================Home Banner Area =================-->
    <section class="banner_area">
		<div class="banner_inner d-flex align-items-center">
			<div class="container">
				<div class="banner_content text-center">
					<h2>Pesanan Diterima</h2>
					<div class="page_link">
            <a href="{{ url('/') }}">Home</a>
						<a href="">Invoice</a>
					</div>
				</div>
			</div>
		</div>
	</section>
	<!--================End Home Banner Area =================-->

	<!--================Order Details Area =================-->
	<section class="order_details p_120">
		<div class="container">
			<h3 class="title_confirmation">Terima kasih, pesanan anda telah kami terima.</h3>
			<div class="row order_d_inner">
				<div class="col-lg-6">
					<div class="details_item">
						<h4>Informasi Pesanan</h4>
						<ul class="list">
							<li>
								<a href="#">
                  <span>Invoice</span> : {{ $order->invoice }}</a>
							</li>
							<li>
								<a href="#">
                  <span>Tanggal</span> : {{ $order->created_at }}</a>
							</li>
							<li>
								<a href="#">
                  <span>Total</span> : Rp {{ number_format($order->subtotal) }}</a>
							</li>
						</ul>
					</div>
				</div>
				<div class="col-lg-6">
					<div class="details_item">
						<h4>Informasi Pemesan</h4>
						<ul class="list">
							<li>
								<a href="#">
                  <span>Alamat</span> : {{ $order->customer_address }}</a>
							</li>
							<li>
								<a href="#">
                  <span>Kota</span> : {{ $order->district->city->name }}</a>
							</li>
							<li>
								<a href="#">
									<span>Country</span> : Indonesia</a>
							</li>
						</ul>
					</div>
				</div>
			</div>
		</div>
	</section>
  <!--================End Order Details Area =================-->
    
@endsection

Definsikan route yang terakhir, buka file routes/web.php dan tambahkan code

Route::get('/checkout/{invoice}', 'Ecommerce\CartController@checkoutFinish')->name('front.finish_checkout');

Baca Juga: Aplikasi E-commerce Laravel 6 #7: Filter Product Detail

Kesimpulan

Sepanjang artikel ini kita telah belajar banyak hal diantaranya bagaimana cara menyimpan data yang ada di Cookie ke dalam database, selain itu kita juga belajar bagaimana menggunakan database transaction untuk meminimalisir kesamaan data jika terjadi error ketika proses penyimpanan dilakukan pada lebih dari satu table yang saling terakait.

Adapun dokumentasi code dari artikel ini bisa dilihat di Github.

Category:
Share:

Comments