Merge branch 'feature-product' into development
WMS API/ECOMMERCE/pipeline/head There was a failure building this commit Details

This commit is contained in:
Bayu Lukman Yusuf 2026-02-26 08:47:45 +07:00
commit 95d0b6cdd6
7 changed files with 123 additions and 55 deletions

View File

@ -32,6 +32,7 @@ class AddressController extends Controller
'postal_code' => $address->postal_code, 'postal_code' => $address->postal_code,
'latitude' => $address->latitude, 'latitude' => $address->latitude,
'longitude' => $address->longitude, 'longitude' => $address->longitude,
'phone' => $address->phone,
]; ];
}); });

View File

@ -10,6 +10,8 @@ use App\Repositories\Member\Transaction\TransactionRepository;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class CheckoutController extends Controller class CheckoutController extends Controller
{ {
public function index(Request $request, MemberCartRepository $memberCartRepository) public function index(Request $request, MemberCartRepository $memberCartRepository)
@ -20,7 +22,7 @@ class CheckoutController extends Controller
$subtotal = $memberCartRepository->getSubtotal($request->input('location_id')); $subtotal = $memberCartRepository->getSubtotal($request->input('location_id'));
$address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary','desc')->get(); $address_list = Address::where('user_id', auth()->user()->id)->orderBy('is_primary','desc')->get();
$total = $subtotal; $total = $subtotal;
@ -92,19 +94,32 @@ class CheckoutController extends Controller
$request->merge(['location_id' => $location_id]); $request->merge(['location_id' => $location_id]);
$carts = $memberCartRepository->getList($request); $carts = $memberCartRepository->getList($request);
$shipping_list = collect($shippingRepository->getList([
"location_id" => $location_id, try{
"address_id" => $address_id, $shipping_list = collect($shippingRepository->getList([
"items" => $carts, "location_id" => $location_id,
])['pricing'])->map(function($row){ "address_id" => $address_id,
return [ "items" => $carts,
'courier' => $row['courier_code'], ])['pricing'] ?? [])->map(function($row){
'service' => $row['courier_service_code'], return [
'title' => $row['courier_name']." - ".$row['courier_service_name'], 'courier' => $row['courier_code'],
'description' => $row['duration'], 'service' => $row['courier_service_code'],
'cost' => $row['shipping_fee'], 'title' => $row['courier_name']." - ".$row['courier_service_name'],
]; 'description' => $row['duration'],
}); 'cost' => $row['shipping_fee'],
];
});
if (count($shipping_list) == 0) {
throw ValidationException::withMessages([
"message" => "Tidak dapat menghitung ongkir"
]);
}
}catch(\Exception $e){
throw ValidationException::withMessages([
"message" => $e->getMessage()
]);
}
return view('checkout.v1-delivery-1-shipping', [ return view('checkout.v1-delivery-1-shipping', [
'carts' => $request->user()->carts, 'carts' => $request->user()->carts,
@ -117,7 +132,7 @@ class CheckoutController extends Controller
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::info($e); Log::info($e);
return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data'); return redirect()->route('checkout.delivery')->with('error', $e->getMessage() ?? 'Invalid checkout data');
} }
} }

View File

@ -96,13 +96,21 @@ class ShippingRepository
}); });
if ($hasLatLong){ if ($hasLatLong){
$list = $biteship->rateByLatLong([ try{
"origin_latitude" => $location->latitude, $list = $biteship->rateByLatLong([
"origin_longitude" => $location->longitude, "origin_latitude" => $location->latitude,
"destination_latitude" => $address->latitude, "origin_longitude" => $location->longitude,
"destination_longitude" => $address->longitude, "destination_latitude" => $address->latitude,
"items" => $items "destination_longitude" => $address->longitude,
]); "items" => $items
]);
}catch(ValidationException $e){
$list = $biteship->rateByPostal([
"origin_postal_code" => $location->postal_code,
"destination_postal_code" => $address->postal_code,
"items" => $items
]);
}
}else{ }else{
$list = $biteship->rateByPostal([ $list = $biteship->rateByPostal([

View File

@ -5,6 +5,7 @@ namespace App\ThirdParty\Biteship;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\ValidationException;
class Rate class Rate
{ {
@ -28,19 +29,29 @@ class Rate
$items) { $items) {
$url = env("BITESHIP_URL"); $url = env("BITESHIP_URL");
$key = env("BITESHIP_KEY"); $key = env("BITESHIP_KEY");
$res = Http::withHeaders([
"authorization" => $key $body = [
])
->withBody(json_encode([
"origin_latitude" => $origin_latitude, "origin_latitude" => $origin_latitude,
"origin_longitude" => $origin_longitude, "origin_longitude" => $origin_longitude,
"destination_latitude" => $destination_latitude, "destination_latitude" => $destination_latitude,
"destination_longitude" => $destination_longitude, "destination_longitude" => $destination_longitude,
"couriers" => env("BITESHIP_COURIER_ALL","grab,gojek,tiki,jnt,anteraja"), "couriers" => env("BITESHIP_COURIER","grab,gojek,tiki,jnt,anteraja"),
"items" => $items "items" => $items
]), 'application/json') ];
$res = Http::withHeaders([
"authorization" => $key
])
->withBody(json_encode($body), 'application/json')
->post($url."/v1/rates/couriers"); ->post($url."/v1/rates/couriers");
if ($res->status() != 200 && $res->json()['error'] != null) {
Log::info(json_encode($res->json()));
Log::info($body);
throw ValidationException::withMessages([
"message" => $res->json()['error']
]);
}
if ($res->status() == 200) if ($res->status() == 200)
return $res->json(); return $res->json();
@ -73,6 +84,12 @@ class Rate
]), 'application/json') ]), 'application/json')
->post($url."/v1/rates/couriers"); ->post($url."/v1/rates/couriers");
if ($res->status() != 200 && $res->json()['error'] != null) {
throw ValidationException::withMessages([
"message" => $res->json()['error']
]);
}
if ($res->status() == 200) if ($res->status() == 200)
return $res->json(); return $res->json();

View File

@ -83,7 +83,7 @@
<td class="py-3 ps-0"> <td class="py-3 ps-0">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a class="flex-shrink-0" <a class="flex-shrink-0"
href="{{ route('product.detail', $cart->itemVariant->item->slug) }}"> href="{{ $cart->itemVariant->item->slug != null ? route('product.detail', $cart->itemVariant->item->slug) : '#' }}">
<img src="{{ $cart->itemReference->item->image_url ?? '' }}" <img src="{{ $cart->itemReference->item->image_url ?? '' }}"
width="80" alt="{{ $cart->name ?? 'Product' }}"> width="80" alt="{{ $cart->name ?? 'Product' }}">
</a> </a>
@ -92,7 +92,7 @@
<div class="w-100 min-w-0 ps-2 ps-xl-3"> <div class="w-100 min-w-0 ps-2 ps-xl-3">
<h5 class="d-flex animate-underline mb-2"> <h5 class="d-flex animate-underline mb-2">
<a class="d-block fs-sm fw-medium text-truncate animate-target" <a class="d-block fs-sm fw-medium text-truncate animate-target"
href="{{ $cart->itemVariant->item->slug != null ? route('product.detail', [$cart->item->slug]) : '#' }}">{{ $cart->itemVariant->display_name ?? ($cart->itemVariant->description ?? 'Product Name') }}</a> href="{{ $cart->itemVariant->item->slug != null ? route('product.detail', [$cart->itemVariant->item->slug]) : '#' }}">{{ $cart->itemVariant->display_name ?? ($cart->itemVariant->description ?? 'Product Name') }}</a>
</h5> </h5>
{{-- <ul class="list-unstyled gap-1 fs-xs mb-0"> {{-- <ul class="list-unstyled gap-1 fs-xs mb-0">
<li><span class="text-body-secondary">Color:</span> <span <li><span class="text-body-secondary">Color:</span> <span

View File

@ -85,7 +85,7 @@
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px"> <aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px"> <div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0" <x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
:showEdit="true" :editUrl="route('second', ['checkout', 'v1-cart'])" /> :showEdit="true" :editUrl="route('cart.index')" />
</div> </div>
</aside> </aside>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="row pt-1 pt-sm-3 pt-lg-4 pb-2 pb-md-3 pb-lg-4 pb-xl-5"> <div class="row pt-1 pt-sm-3 pt-lg-4 pb-2 pb-md-3 pb-lg-4 pb-xl-5">
{{-- show error message --}} {{-- show error message --}}
@if(session('error')) @if (session('error'))
<div class="alert alert-danger"> <div class="alert alert-danger">
{{ session('error') }} {{ session('error') }}
</div> </div>
@ -84,13 +84,14 @@
data-state="{{ $address->province->name }}" data-state="{{ $address->province->name }}"
data-postcode="{{ $address->postal_code }}" data-postcode="{{ $address->postal_code }}"
data-phone="{{ $address->phone }}" data-phone="{{ $address->phone }}"
data-latitude="{{ $address->latitude }}"
data-longitude="{{ $address->longitude }}"
{{ $address->is_primary || $key === 0 ? 'selected' : '' }}> {{ $address->is_primary || $key === 0 ? 'selected' : '' }}>
{{ $address->label }} - {{ $address->name }}, {{ $address->label }} - {{ $address->name }},
{{ $address->address }}, {{ $address->city_name }}, {{ $address->address }}
{{ $address->province_name }} {{ $address->postal_code }}
@if ($address->is_primary) @if ($address->is_primary)
<span <span
class="badge bg-primary ms-2">{{ __('checkout.primary') }}</span> class="badge bg-primary ms-2">({{ __('checkout.primary') }})</span>
@endif @endif
</option> </option>
@endforeach @endforeach
@ -100,7 +101,12 @@
<!-- Shipping Address (shown automatically) --> <!-- Shipping Address (shown automatically) -->
<div id="shippingAddress" class="shipping-address mt-4" style="display: block;"> <div id="shippingAddress" class="shipping-address mt-4" style="display: block;">
<h6 class="mb-3">{{ __('checkout.shipping_address') }}</h6> <div class="d-flex justify-content-between">
<h6 class="mb-3">{{ __('checkout.shipping_address') }}</h6>
<a type="button" href="{{ route('addresses') }}"
class="btn btn-outline-primary btn-sm">Edit</a>
</div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label for="firstName" <label for="firstName"
@ -108,11 +114,17 @@
<input type="text" class="form-control" id="firstName" readonly <input type="text" class="form-control" id="firstName" readonly
style="border: none; background-color: #f8f9fa;"> style="border: none; background-color: #f8f9fa;">
</div> </div>
<div class="col-md-6"> {{-- <div class="col-md-6">
<label for="lastName" <label for="lastName"
class="form-label">{{ __('checkout.last_name') }}</label> class="form-label">{{ __('checkout.last_name') }}</label>
<input type="text" class="form-control" id="lastName" readonly <input type="text" class="form-control" id="lastName" readonly
style="border: none; background-color: #f8f9fa;"> style="border: none; background-color: #f8f9fa;">
</div> --}}
<div class="col-12">
<label for="phone"
class="form-label">{{ __('checkout.phone') }}</label>
<input type="tel" class="form-control" id="phone" readonly
style="border: none; background-color: #f8f9fa;">
</div> </div>
<div class="col-12"> <div class="col-12">
<label for="address" <label for="address"
@ -138,12 +150,19 @@
<input type="text" class="form-control" id="zip" readonly <input type="text" class="form-control" id="zip" readonly
style="border: none; background-color: #f8f9fa;"> style="border: none; background-color: #f8f9fa;">
</div> </div>
<div class="col-12"> <div class="col-md-6">
<label for="phone" <label for="latitude"
class="form-label">{{ __('checkout.phone') }}</label> class="form-label">Latitude</label>
<input type="tel" class="form-control" id="phone" readonly <input type="text" class="form-control" id="latitude" readonly
style="border: none; background-color: #f8f9fa;"> style="border: none; background-color: #f8f9fa;">
</div> </div>
<div class="col-md-6">
<label for="longitude"
class="form-label">Longitude</label>
<input type="text" class="form-control" id="longitude" readonly
style="border: none; background-color: #f8f9fa;">
</div>
</div> </div>
</div> </div>
</div> </div>
@ -170,8 +189,10 @@
<div class="mt-4"> <div class="mt-4">
<form action="{{ route('checkout.delivery.process') }}" method="post"> <form action="{{ route('checkout.delivery.process') }}" method="post">
@csrf @csrf
<input type="hidden" name="delivery_method" id="deliveryMethodInput" value="shipping"> <input type="hidden" name="delivery_method" id="deliveryMethodInput"
<input type="hidden" name="address_id" id="addressIdInput" value="{{ $address_list->first()->id }}"> value="shipping">
<input type="hidden" name="address_id" id="addressIdInput"
value="{{ $address_list->first()->id }}">
<button type="submit" class="btn btn-lg btn-primary w-100" <button type="submit" class="btn btn-lg btn-primary w-100"
id="continueButton"> id="continueButton">
<span <span
@ -203,14 +224,14 @@
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px"> <aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px"> <div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0" <x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
:showEdit="true" :editUrl="route('second', ['checkout', 'v1-cart'])" /> :showEdit="true" :editUrl="route('cart.index')" />
</div> </div>
</aside> </aside>
</div> </div>
</div> </div>
</main> </main>
@include('layouts.partials/footer') @include('layouts.partials/footer2')
@endsection @endsection
@section('scripts') @section('scripts')
@ -231,7 +252,8 @@
pickupOptions.style.display = 'none'; pickupOptions.style.display = 'none';
resetPickupSelection(); resetPickupSelection();
// Update button text for delivery // Update button text for delivery
document.getElementById('continueButtonText').textContent = '{{ __("checkout.continue_to_shipping") }}'; document.getElementById('continueButtonText').textContent =
'{{ __('checkout.continue_to_shipping') }}';
// Update hidden input values // Update hidden input values
document.getElementById('deliveryMethodInput').value = 'delivery'; document.getElementById('deliveryMethodInput').value = 'delivery';
// Update address ID from selected address // Update address ID from selected address
@ -309,13 +331,14 @@
if (selectedOption && selectedOption.value) { if (selectedOption && selectedOption.value) {
// Populate all shipping address form fields // Populate all shipping address form fields
document.getElementById('firstName').value = selectedOption.dataset.firstName || ''; document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || ''; // document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || ''; document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || ''; document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || ''; document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || ''; document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || ''; document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || ''; document.getElementById('latitude').value = selectedOption.dataset.latitude || '';
document.getElementById('longitude').value = selectedOption.dataset.longitude || '';
// Update order summary with postcode // Update order summary with postcode
if (selectedOption.dataset.postcode) { if (selectedOption.dataset.postcode) {
@ -337,25 +360,29 @@
if (this.value === 'new' || this.value === '') { if (this.value === 'new' || this.value === '') {
// Clear form fields for new address // Clear form fields for new address
document.getElementById('firstName').value = ''; document.getElementById('firstName').value = '';
document.getElementById('lastName').value = ''; // document.getElementById('lastName').value = '';
document.getElementById('address').value = ''; document.getElementById('address').value = '';
document.getElementById('city').value = ''; document.getElementById('city').value = '';
document.getElementById('state').value = ''; document.getElementById('state').value = '';
document.getElementById('zip').value = ''; document.getElementById('zip').value = '';
document.getElementById('phone').value = ''; document.getElementById('phone').value = '';
document.getElementById('postcode').value = ''; document.getElementById('latitude').value = '';
document.getElementById('longitude').value = '';
document.getElementById('zip').value = '';
// Clear address ID input // Clear address ID input
document.getElementById('addressIdInput').value = ''; document.getElementById('addressIdInput').value = '';
} else { } else {
// Populate form fields with selected address // Populate form fields with selected address
document.getElementById('firstName').value = selectedOption.dataset.firstName || ''; document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || ''; // document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || ''; document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || ''; document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || ''; document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || ''; document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || ''; document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || ''; document.getElementById('latitude').value = selectedOption.dataset.latitude || '';
document.getElementById('longitude').value = selectedOption.dataset.longitude || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || '';
// Update address ID input // Update address ID input
document.getElementById('addressIdInput').value = this.value; document.getElementById('addressIdInput').value = this.value;
@ -368,7 +395,7 @@
} }
// Auto-update order summary when postcode changes // Auto-update order summary when postcode changes
document.getElementById('postcode').addEventListener('input', function() { document.getElementById('zip').addEventListener('input', function() {
if (this.value) { if (this.value) {
updateOrderSummaryWithShipping(); updateOrderSummaryWithShipping();
} }
@ -427,7 +454,7 @@
function updateOrderSummaryWithShipping() { function updateOrderSummaryWithShipping() {
// Simulate shipping cost calculation based on postcode // Simulate shipping cost calculation based on postcode
const shippingCost = calculateShippingCost(document.getElementById('postcode').value); const shippingCost = calculateShippingCost(document.getElementById('zip').value);
// Update order summary // Update order summary
const subtotalElement = document.getElementById('cart-subtotal'); const subtotalElement = document.getElementById('cart-subtotal');
@ -510,7 +537,7 @@
} }
function validateDeliveryForm() { function validateDeliveryForm() {
const postcode = document.getElementById('postcode').value; const postcode = document.getElementById('zip').value;
const firstName = document.getElementById('firstName').value; const firstName = document.getElementById('firstName').value;
const address = document.getElementById('address').value; const address = document.getElementById('address').value;
const city = document.getElementById('city').value; const city = document.getElementById('city').value;